Android-自定义控件~支付宝支付的动画特效

引子

话不多说,先上图,继续研究 动画特效。看了一些人的源码,自己写了一个。以后,对于下面这种效果,或者类似下面效果但是更加复杂的特效,也不至于没思路了。

 

动画拆分

动态图中可以看到,整个效果分为两个部分,一个是,外层圆弧,一个是内层对勾 √ .

外层圆弧又分为三个动画阶段,

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 }
View Code

 

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 }
View Code

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>
View Code

 

结语

凡是看似复杂的动画,都可以拆分为简单动画的组合,所以看到掉渣天的特效,也没什么好怕的,慢慢拆分就行了。

不是什么大项目,就不提供github了,拷贝到项目内能直接使用,就这样。

 

posted @ 2018-10-10 11:19  波澜不惊x  阅读(1109)  评论(0编辑  收藏  举报