利用 PathMeasure 实现路径动画 - 支付宝支付动画
1. PathMeasure 是什么?
-
它类似一个计算器,可以计算出指定路径的一些信息,比如路径总长、指定长度所对应的坐标点等;
-
初始化方法;
//方法一 PathMeasure pathMeasure = new PathMeasure() ; setPath (Path path , boolean forceClosed ); //方法二 PathMeasure(Path path , boolean forceClosed;
其中 forceClosed 表示,待计算的 path 长度,是否需要让其闭合计算。
2. PathMeasure 的常用函数
2.1 get length() 函数;
- 声明:
public float getLength ();
- 其作用就是获取计算的路径长度
2.2 getSegment ()函数
- 声明
boolean getSegrneηt ( float startD, float stopD , Path dst , boolean startWithMoveTo )
- 其作用是用于截取整个Path 中的某个片段,将截取后的 Path 保存( 添加 )到参数dst 中;
- 参数
startD
表示 开始截取位置距离 Path 起始点 的长度;
参数stopD
表示 结束截取位置距离 Path 起始点 的长度;
参数startWithMoveTo
表示 起始点是否使用 To 将路径的新起始点移到结果 Path 的起始点,通常设置为 true;
2.3 getPosTan ()函数
- 声明:
boolean getPosTan(float distance , float[] pos , float[) tan);
- 其作用是 得到路径上某一长度的位置以及该位置的正切;
- 参数
distance
表示 距离 Path 起始点的长度;
参数pos
表示 该点的坐标;pos[0 ]表示 x 坐标, pos[1]表示 y 坐标。
参数该点的正切
表示 该点的正切值; - 注意:getPosTan () 函数中获取的正切值也是一个二维数组;这个数组标识的点是,以 1 为半径的圆上的点,通过反正切获取其对应的角度;
- 在Math 类中,有两个求反正切值的函数。
注意此处的参数 d 是 弧度值;double atan (double d); double atan2 (double y, double x);
2.4 getMatrix ()函数
-
声明:
boolean getMatrix(float d工stance , Matrix matrix , int flags);
-
其作用是:用于得到路径上某一长度的位置以及该位置的正切值的矩阵;
-
参数
distance
表示 距离Path 起始点的长度。
参数matrix
表示 根据 flags 封装好的 matrix 会根据 flags 的设置而存入不同的内容。
参数flags
表示 用于指定哪些内容会存入 matrix 中。flags 的值有两个:
pathMeasure.POSITION_MATRIX_FLAG
表示获取位置信息;
pathMeasure. TANGENT_MATRIX_FLAG
表示获取切边信息,使得图片按Path 旋转。可以只指定一个,也可以使用" I '’(或运算符〉
同时指定。 -
getMatrix()
函数是PathMeasure.getPosTan()
函数的另一种实现;
3. 利用 PathMeasure 实现路径动画 - 小示例
- 已知:,通过 getSegment() 函数可以根据路径的长度截取对应的路径线段。所以只需不断地给 getSegment() 函数设置逐渐增长的路径长度,就会相应得到逐渐增长的路径线段,把这个路径线段实时地画出来就可以了。
- 当箭头围绕圆形旋转时,应该实时地旋转箭头的转向;偏转角度即所在点的切线;具体角度根据图片的位置确定,这里的箭头本来就是向下的倒三角形,起始位置在右侧水平位置,其初始角度应为 -90°;
3.1 自定义一个 View
-
代码如下:
public class GetSegmentView extends View { private Paint mPaint; private Path mCirclePath; private Path mDstPath; private PathMeasure mPathMeasure; private Float mCurAnimValue; private float mStop; private Bitmap mBitmap; private int mRadius = 60; private int mCentX = 70; private int mCentY = 70; private float[] pos = new float[2]; private float[] tan = new float[2]; public GetSegmentView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); //禁用硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null); mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.down_arrow_popup_item); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(Color.WHITE); mPaint.setStrokeWidth(4); mCirclePath = new Path(); mDstPath = new Path(); mCirclePath.addCircle(mCentX, mCentY, mRadius, Path.Direction.CW); mPathMeasure = new PathMeasure(mCirclePath, true); ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mCurAnimValue = (Float) animation.getAnimatedValue(); invalidate(); } }); valueAnimator.setDuration(2000); valueAnimator.start(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }
-
重写 onDraw() 方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画出加载圆动画 mStop = mPathMeasure.getLength() * mCurAnimValue; mDstPath.reset(); mPathMeasure.getSegment(0f, mStop, mDstPath, true); canvas.drawPath(mDstPath, mPaint); //用 getPosTan() 获取图像旋转角度,画出加载动画的“箭头” mPathMeasure.getPosTan(mStop, pos, tan); float degree = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI) - 90.0f; Matrix matrix = new Matrix(); matrix.postRotate(degree, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2); matrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2); canvas.drawBitmap(mBitmap, matrix, mPaint); //用 getMatrix() 获取图像旋转角度,画出加载动画的“箭头” //Matrix matrix2 = new Matrix(); //mPathMeasure.getMatrix(mStop, matrix2, PathMeasure.POSITION_MATRIX_FLAG | // PathMeasure.TANGENT_MATRIX_FLAG); //matrix2.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2); //canvas.drawBitmap(mBitmap, matrix, mPaint); }
-
在布局文件中引用。
4. 支付动画分析
- 圆形和对钩是两条完全相连的路径;在 Path 变量中添加圆形、对勾两条路径;
- 在画完圆形以后, 需要利用
PathMeasure. nextContour()
函数将 Path 转到对钩路径继续。
4.1 自定义 View 构造代码
-
代码
public class PayView extends View { private Paint mPaint; private Path mCirclePath; private Path mDstPath; private PathMeasure mPathMeasure; private Float mCurAnimValue; private float mStop; private int mRadius = 60; private int mCentX = 70; private int mCentY = 70; private static final String TAG = PayView.class.getClass().getSimpleName(); public PayView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); //禁用硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(Color.WHITE); mPaint.setStrokeWidth(4); mCirclePath = new Path(); mDstPath = new Path(); mCirclePath.addCircle(mCentX, mCentY, mRadius, Path.Direction.CW); //添加对勾的 Path mCirclePath.moveTo(mCentX - mRadius / 2, mCentY); mCirclePath.lineTo(mCentX, mCentY + mRadius / 2); mCirclePath.lineTo(mCentX + mRadius / 2, mCentY - mRadius / 3); mPathMeasure = new PathMeasure(mCirclePath, false); //设置为 0 - 2 ;当动画在 0 - 1 时画圆;1 - 2 时画对勾 //注意:此处设置动画循环次数是失效的,(不要问我,我也不知道为啥。) ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 2f); valueAnimator.setDuration(3000); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mCurAnimValue = (Float) animation.getAnimatedValue(); invalidate(); } }); valueAnimator.start(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }
4.2 重写 onDraw() 方法
- 代码
//这里默认他已经绘制完圆形 private boolean isCircleDrawed = true; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mCurAnimValue < 1) { //画出加载 圆 动画 mStop = mPathMeasure.getLength() * mCurAnimValue; mPathMeasure.getSegment(0f, mStop, mDstPath, true); //Log.i(TAG, " mCurAnimValue = " + mCurAnimValue + " ; onDraw : mStop = " + mStop); } else { //这里原本判断的是 mCurAnimValue == 1 时,和 mCurAnimValue > 1 时,但是判断中无法进入 mCurAnimValue == 1; if (isCircleDrawed) { //画完圆圈、并移动到下一个动画 Path; mPathMeasure.getSegment(0f, mPathMeasure.getLength(), mDstPath, true); mPathMeasure.nextContour(); //Log.i(TAG, " mCurAnimValue = " + mCurAnimValue + " ; onDraw:mStop = " + mStop); isCircleDrawed = false; } //画出圆内对勾 mStop = mPathMeasure.getLength() * (mCurAnimValue - 1); mPathMeasure.getSegment(0f, mStop, mDstPath, true); //Log.i(TAG, " mCurAnimValue = " + mCurAnimValue + " ; onDraw:mStop = " + mStop); } canvas.drawPath(mDstPath, mPaint); }
4.3 注意
- 动画进行中,返回 Float 类型的动画进度,此处本来比较的时进度 == 1 时,将圆画完,之后画对勾;
- 但在实际过程中,动画进度并没有完全 == 1 ,
- 当涉及到 float 类型的逐渐递增,最好不要使用 float f == int i 这种比较操作;因为基本上,f = 0.99450225 和 1.0094247 时,是无法进入的。
声明:本文整理自《《Android自定义控件开发入门与实战》_启舰》;