引子
话不多说,先上图,继续研究 动画特效。看了一些人的源码,自己写了一个。以后,对于下面这种效果,或者类似下面效果但是更加复杂的特效,也不至于没思路了。
动画拆分
动态图中可以看到,整个效果分为两个部分,一个是,外层圆弧,一个是内层对勾 √ .
外层圆弧又分为三个动画阶段,
1) 圆弧的角度从0,到达设定好的最大值(我用的是200度),
2) 到达200度之后,整个圆弧会顺时针推进,直到到达Y轴正向的位置,
3)最后一步,圆弧逐渐缩小直到角度为0
内层对勾√
1)直线1的逐步绘制
2)直线2的逐步绘制
源代码
下面是带详尽注释的自定义View源码
1 package com.example.my_alipay_view; 2 3 import android.content.Context; 4 import android.graphics.Canvas; 5 import android.graphics.Color; 6 import android.graphics.Paint; 7 import android.graphics.PointF; 8 import android.graphics.RectF; 9 import android.support.annotation.Nullable; 10 import android.util.AttributeSet; 11 import android.view.View; 12 13 public class MyAliPayView extends View { 14 15 //****** 3个构造函数,无需赘述 ************* 16 public MyAliPayView(Context context) { 17 this(context, null); 18 } 19 20 public MyAliPayView(Context context, @Nullable AttributeSet attrs) { 21 this(context, attrs, 0); 22 } 23 24 public MyAliPayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 25 super(context, attrs, defStyleAttr); 26 init(); 27 } 28 29 //***********全局变量设定************* 30 private Paint mPaint;//画笔 31 32 //圆弧(这里我用是正圆的圆弧)相关参数 33 private int mCenterX, mCenterY; //圆心位置的X,Y 34 private int mRadio = 150;//半径,圆弧半径 35 private RectF mRectArc = new RectF();//画弧线用的辅助矩形 36 private final float mDeltaAngle = 10;//每次刷新时,角度的变化,这个变量影响动画的速率 37 private final float mMaxSwipeAngle = 200;// 确定一个最大角度 38 private float mCurrentSwipeAngle = 0;//弧线当前扫过的角度 39 private float mStartAngle = 0;//一旦当前角度达到最大值,那么不再增加,而是进行旋转 40 private ProgressTag mProgressTag = ProgressTag.PROGRESS_0;//弧线绘制的过程 41 42 //画对勾 √ 相关参数 43 private float x1, y1, x2, y2, x3, y3;//构成对勾的3个坐标(x1,y1)(x2,y2)(x3,y3) 44 private float mDeltaLineDis = 10;// 勾勾在X轴上每次刷新移动的距离,这个值影响对勾的绘制速率 45 private Line line1, line2;// 两段直线 46 private float xDis3to1;// Point3 到 point1 的X跨度(它的作用是 用来限制对勾第二条直线的绘制范围) 47 private float xDis2to1;// Point2 到 point1 的X跨度(它的作用是 用来限制对勾第一条直线的绘制范围) 48 private float mCurrentXDis;// 画√的时候X轴跨度 49 50 private State mCurrentStatus = State.STATUS_IDLE;//默认闲置状态 51 52 /** 53 * 设置状态(状态决定动画效果) 54 * 55 * @param currentStatus 56 */ 57 public void setStatus(State currentStatus) { 58 this.mCurrentStatus = currentStatus; 59 invalidate(); 60 } 61 62 @Override 63 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 64 super.onSizeChanged(w, h, oldw, oldh); 65 mCenterX = w / 2; 66 mCenterY = h / 2; 67 mRectArc.set(mCenterX - mRadio, mCenterY - mRadio, mCenterX + mRadio, mCenterY + mRadio); 68 69 //确定对勾的两条Line的起点和终点 70 x1 = mCenterX - mRadio / 3 * 2; 71 y1 = mCenterY + mRadio / 8; 72 x2 = mCenterX - mRadio / 5; 73 y2 = mCenterY + mRadio / 3 * 2; 74 x3 = mCenterX + mRadio / 4 * 3; 75 y3 = mCenterY - mRadio / 4; 76 //确定两条直线; //把勾勾一次性画出来倒是不难,难的是,这个动态过程如何写 77 line1 = new Line(new PointF(x1, y1), new PointF(x2, y2)); 78 line2 = new Line(new PointF(x2, y2), new PointF(x3, y3)); 79 80 xDis3to1 = x3 - x1; 81 xDis2to1 = x2 - x1; 82 } 83 84 @Override 85 protected void onDraw(Canvas canvas) { 86 super.onDraw(canvas); 87 switch (mCurrentStatus) { 88 case STATUS_IDLE://闲置状态,画出完整图形即可 89 reset(); 90 canvas.drawArc(mRectArc, -90 + mStartAngle, 360, false, mPaint);//画出完整圆弧360度 91 canvas.drawLine(line1.startP.x, line1.startP.y, line1.endP.x, line1.endP.y, mPaint);//第1条直线,保持完整 92 canvas.drawLine(line2.startP.x, line2.startP.y, line2.endP.x, line2.endP.y, mPaint);//第2条直线,保持完整 93 break; 94 case STATUS_PROCESS://执行中状态,两种弧线轮流绘制 95 //这里有个-90,这是因为画弧线,默认是从第一象限X正向开始画,我要从Y正向开始顺时针的话,就必须把起点逆时针90度 96 canvas.drawArc(mRectArc, -90 + mStartAngle, mCurrentSwipeAngle, false, mPaint); 97 98 //两个阶段轮流执行 99 if (mProgressTag == ProgressTag.PROGRESS_0) { 100 if (mCurrentSwipeAngle <= mMaxSwipeAngle)//如果扫过角度小于最大角度 101 mCurrentSwipeAngle += mDeltaAngle;//就让扫过的角度继续递增 102 else//如果扫过角度到达了最大角度 103 mStartAngle += mDeltaAngle;//那就让起始角度值递增 104 if (mStartAngle + mCurrentSwipeAngle >= 360) {// 如果弧线末端到达了Y轴正向 105 mProgressTag = ProgressTag.PROGRESS_1;//就切换成阶段1 106 } 107 } else if (mProgressTag == ProgressTag.PROGRESS_1) {//阶段1: 108 mCurrentSwipeAngle -= mDeltaAngle;//扫过角度递减 109 mStartAngle += mDeltaAngle;//起始角度值递增 110 111 if (mCurrentSwipeAngle <= 0) {//如果起始角度值 递减直至0 112 mStartAngle = 0; 113 mProgressTag = ProgressTag.PROGRESS_0;//就切换阶段0 114 } 115 } 116 invalidate();//这种模式下,动画总是要循环执行,所以,无条件刷新 117 break; 118 case STATUS_FINISH:// 已完成状态 119 // 先画一个完整圆弧,然后画对勾 120 //这里有两个阶段, 121 canvas.drawArc(mRectArc, -90 + mStartAngle, mCurrentSwipeAngle, false, mPaint);//以当前两个参数继续画圆弧,直到画完整 122 // 接着当前的圆弧进行绘制 123 mCurrentSwipeAngle += mDeltaAngle;//扫过的角度递增 124 if (mCurrentSwipeAngle <= 360)// 如果扫过角度没达到了360°,就继续递增 125 invalidate(); 126 else {//如果扫过角度,超过了360°,说明圆弧已经完整 127 //这里就开始画 对勾 128 // 对勾包括两条直线 line1,line2 ,思路为:line1的起点X开始向右扫描,逐步画出第一条直线,直到线段1的X,然后逐步画出第二条直线 129 mCurrentXDis += mDeltaLineDis;// 画√的时候X轴跨度 130 if (mCurrentXDis < xDis2to1) {//跨度在第一条直线的跨度范围之内时 131 canvas.drawLine(line1.startP.x, line1.startP.y, line1.startP.x + mCurrentXDis, line1.getY(line1.startP.x + mCurrentXDis), mPaint); 132 invalidate();//继续刷新 133 } else if (mCurrentXDis < xDis3to1) {//跨度越过了第一条直线 到达 第二条直线范围内时 134 canvas.drawLine(line1.startP.x, line1.startP.y, line1.endP.x, line1.endP.y, mPaint);//第一条直线,保持完整 135 canvas.drawLine( 136 line2.startP.x, 137 line2.startP.y, 138 line2.startP.x + mCurrentXDis - xDis2to1, 139 line2.getY(line2.startP.x + mCurrentXDis - xDis2to1), 140 mPaint);//动态画第二条直线 141 invalidate();//继续刷新 142 } else {//跨度超过了,那么就画出完整图形,并且不再刷新 143 canvas.drawLine(line1.startP.x, line1.startP.y, line1.endP.x, line1.endP.y, mPaint);//第1条直线,保持完整 144 canvas.drawLine(line2.startP.x, line2.startP.y, line2.endP.x, line2.endP.y, mPaint);//第2条直线,保持完整 145 reset(); 146 } 147 } 148 149 break; 150 } 151 152 } 153 154 private void reset() { 155 mCurrentSwipeAngle = 0; 156 mStartAngle = 0; 157 mCurrentXDis = 0; 158 } 159 160 private void init() { 161 mPaint = new Paint(); 162 mPaint.setColor(Color.BLUE); 163 mPaint.setAntiAlias(true);//抗锯齿 164 mPaint.setStyle(Paint.Style.STROKE); 165 mPaint.setStrokeWidth(10);//画笔宽度 166 mPaint.setStrokeCap(Paint.Cap.ROUND);//画直线的时候让线头呈现圆角 167 } 168 169 /** 170 * 确定一条直线 171 * 172 * 直线的公式是 Y = kX + b 173 * 174 * 剩下的,,,懂得都懂,不懂的,问初中老师吧 囧! 175 */ 176 class Line { 177 float k; 178 float b; 179 180 PointF startP, endP; 181 182 Line(PointF startP, PointF endP) { 183 this.startP = startP; 184 this.endP = endP; 185 186 //算出k,b 187 k = (endP.y - startP.y) / (endP.x - startP.x); 188 b = endP.y - k * endP.x; 189 } 190 191 // 由于动态效果需要画对勾的过程,所以我要得到任意点的Y值 192 float getY(float x) { 193 return k * x + b; 194 } 195 } 196 197 /** 198 * 画圆弧时有两个阶段 199 */ 200 enum ProgressTag { 201 PROGRESS_0,//阶段0:顺时针画弧线,直到弧线终点到达Y轴正向; 202 PROGRESS_1//阶段1:弧线划过的角度递减 203 } 204 205 /** 206 * 用枚举来做状态区分 207 */ 208 enum State { 209 STATUS_IDLE,//状态0:初始状态 210 STATUS_PROCESS,//状态1:执行中 211 STATUS_FINISH//状态2:已完成 212 } 213 }
Activity代码
1 import android.os.Bundle; 2 import android.support.v7.app.AppCompatActivity; 3 import android.view.View; 4 import android.widget.Button; 5 6 public class MainActivity extends AppCompatActivity implements View.OnClickListener { 7 8 Button btn_process, btn_finished, btn_idle; 9 MyAliPayView myAlipayView; 10 11 @Override 12 protected void onCreate(Bundle savedInstanceState) { 13 super.onCreate(savedInstanceState); 14 setContentView(R.layout.activity_main); 15 16 myAlipayView = findViewById(R.id.myAlipayView); 17 btn_idle = findViewById(R.id.btn_idle); 18 btn_process = findViewById(R.id.btn_process); 19 btn_finished = findViewById(R.id.btn_finished); 20 btn_process.setOnClickListener(this); 21 btn_finished.setOnClickListener(this); 22 btn_idle.setOnClickListener(this); 23 } 24 25 @Override 26 public void onClick(View v) { 27 switch (v.getId()) { 28 case R.id.btn_idle: 29 myAlipayView.setStatus(MyAliPayView.State.STATUS_IDLE); 30 break; 31 case R.id.btn_process: 32 myAlipayView.setStatus(MyAliPayView.State.STATUS_PROCESS); 33 break; 34 case R.id.btn_finished: 35 myAlipayView.setStatus(MyAliPayView.State.STATUS_FINISH); 36 break; 37 default: 38 break; 39 } 40 } 41 }
xml
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:tools="http://schemas.android.com/tools" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent" 6 android:orientation="vertical" 7 tools:context=".MainActivity"> 8 9 10 <com.example.my_alipay_view.MyAliPayView 11 android:id="@+id/myAlipayView" 12 android:layout_width="match_parent" 13 android:layout_height="300dp" /> 14 15 <LinearLayout 16 android:layout_width="match_parent" 17 android:layout_height="wrap_content" 18 android:gravity="center" 19 android:orientation="horizontal"> 20 21 <Button 22 android:id="@+id/btn_idle" 23 android:layout_width="wrap_content" 24 android:layout_height="wrap_content" 25 android:text="初始状态" /> 26 27 <Button 28 android:id="@+id/btn_process" 29 android:layout_width="wrap_content" 30 android:layout_height="wrap_content" 31 android:text="执行中" /> 32 33 <Button 34 android:id="@+id/btn_finished" 35 android:layout_width="wrap_content" 36 android:layout_height="wrap_content" 37 android:text="已完成" /> 38 </LinearLayout> 39 40 </LinearLayout>
结语
凡是看似复杂的动画,都可以拆分为简单动画的组合,所以看到掉渣天的特效,也没什么好怕的,慢慢拆分就行了。
不是什么大项目,就不提供github了,拷贝到项目内能直接使用,就这样。