• 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了,拷贝到项目内能直接使用,就这样。

  • 相关阅读:
    [hadoop](2) MapReducer:Distributed Cache
    [hadoop](1) MapReduce:ChainMapper
    hadoop平台搭建
    postgresql主从同步配置
    问题记录-java图片验证码显示乱码
    windows mongodb启动
    新的开始
    springboot和Redis整合
    springboot的简单热部署
    springmvc模式下的上传和下载
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/9765331.html
Copyright © 2020-2023  润新知