作为国民级应用,想必大家对于支付宝并不陌生,作为一名程序猿,经常会被支付宝各种绚丽的界面所吸引,不禁深思,这个功能如何使实现?就好比支付宝的支付动画,究竟是怎么完成的呢?
PathMeasure
PathMeasure在android中,就好比一个测量仪,用来计算Path的一些信息,包括长度,坐标等信息。
构造方法
方法 | 解释 |
---|---|
PathMeasure() | 无参构造方法,创建一个空的PathMeasure |
PathMeasure(Path path, boolean forceClosed) | 有参构造方法,创建一个关联Path的PathMeasure |
常用方法
返回值 | 方法名 | 解释 |
---|---|---|
float | getLength() | 使用非常高广泛,计算路径的长度 |
boolean | isClosed() | 判断测量Path时是否计算闭合 |
boolean | getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) | 通过前两个参数控制截取长度,保存到dst中 |
boolean | getPosTan(float distance, float[] pos, float[] tan) | 得到路径某长度位置及正切值 |
boolean | getMatrix(float distance, Matrix matrix, int flags) | 得到路径某长度位置及正切值的矩阵 |
效果图
初始化
先设定三种状态,分别对应加载、成功和失败,后面会用到
public static final int ALIPLAY_LOADING = 0;
public static final int ALIPLAY_SUCCESS = 1;
public static final int ALIPLAY_FAILED = 2;
三种状态都是一个圆形状,为了方便这里把圆心和半径直接写死,并且初始化Paint
private float radius = 100;
private int centerX = 200;
private int centerY = 200;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);
准备工作之后,先画一个圆,无论是加载、成功或失败,这个圆都是必不可少的,通过Path画圆
circlePath = new Path();
circlePath.addCircle(centerX, centerY, radius, Path.Direction.CW);
既然圆已经画出来了,不妨把对号和错号也画出来,无论是对号还是错号都不难,只要把点的对标计算出来,在通过Path画出来就简单得多了
对号
truePath = new Path();
truePath.moveTo(centerX - radius / 2, centerY);
truePath.lineTo(centerX, centerY + radius / 2);
truePath.lineTo(centerX + radius / 2, centerY - radius / 3);//可以画个草图,很容易理解
错号
错号需要注意以下,两条相交的直线,是根据四个点画出来的
warPath1 = new Path();
warPath1.moveTo(centerX - radius / 2, centerY - radius / 2);
warPath1.lineTo(centerX + radius / 2, centerY + radius / 2);
warPath2 = new Path();
warPath2.moveTo(centerX + radius / 2, centerY - radius / 2);
warPath2.lineTo(centerX - radius / 2, centerY + radius / 2);
要点
本文支付动画都是根据PathMeasure实现,在上面常用方法的表格中讲到了getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
根据startD和stopD截取保存到dst中,因此还需要创建四个dstPath
dstCirclePath = new Path();
dstTruePath = new Path();
dstWarPath1 = new Path();
dstWarPath2 = new Path();//错号的两条Path,因此对应两个dst
circlePathMeasure = new PathMeasure(circlePath, false);
trueMeasure = new PathMeasure(truePath, false);
warPathMeasure1 = new PathMeasure(warPath1, false);
warPathMeasure2 = new PathMeasure(warPath2, false);
加载动画
通过动态图可以发现,加载动画是一个无限循环的状态,因此先创建一个循环动画,并且通过获取动画的完成进度,不断的重绘dstCirclePath
circleAnimator = ValueAnimator.ofFloat(0, 1);
circleAnimator.setDuration(2000);
circleAnimator.setRepeatCount(ValueAnimator.INFINITE);
circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//获取动画完成进度0·1
circleCurValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
circleAnimator.start();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 当进度未0.5-1时,路径起始点都是0,当进度为0.5-1时,起始点逐渐到达终点,当进度为1时, 起始重合
*/
if (viewStatue == Contants.ALIPLAY_LOADING) {
float loadStop = circleCurValue * circlePathMeasure.getLength();
float loadStart = (float) (loadStop - (0.5 - Math.abs(circleCurValue - 0.5)) * circlePathMeasure.getLength());
dstCirclePath.reset();
circlePathMeasure.getSegment(loadStart, loadStop, dstCirclePath, true);
canvas.drawPath(dstCirclePath, paint);
}
}
成功动画
成功动画只是在加载动画的基础上稍微做了下改动,把外圆的无限循环去掉,不断重绘直到形成一个完整的圆,之后开始对号的动画与重绘
circleAnimator = ValueAnimator.ofFloat(0, 1);
circleAnimator.setDuration(2000);
circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
circleCurValue = (float) valueAnimator.getAnimatedValue();
invalidate();
if (circleCurValue == 1) {//当外圆动画完成后,对号动画开始启动
trueAnimator.start();
}
}
});
circleAnimator.start();
trueAnimator = ValueAnimator.ofFloat(0, 1);
trueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
trueCurValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
trueAnimator.setDuration(2000);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (viewStatue == Contants.ALIPLAY_SUCCESS) {
//与加载的区别,从0-1不断重绘
float circleStop = circlePathMeasure.getLength() * circleCurValue;
dstCirclePath.reset();
circlePathMeasure.getSegment(0, circleStop, dstCirclePath, true);
canvas.drawPath(dstCirclePath, paint);
float trueStop = trueMeasure.getLength() * trueCurValue;
dstTruePath.reset();
trueMeasure.getSegment(0, trueStop, dstTruePath, true);
canvas.drawPath(dstTruePath, paint);
}
}
失败动画
失败动画和成功动画差不多,只是注意下错号两条直线的动画的先后顺序即可
circleAnimator = ValueAnimator.ofFloat(0, 1);
circleAnimator.setDuration(2000);
circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
circleCurValue = (float) valueAnimator.getAnimatedValue();
invalidate();
if (circleCurValue == 1) {
warAnimator1.start();
}
}
});
circleAnimator.start();
warAnimator1 = ValueAnimator.ofFloat(0, 1);
warAnimator1.setDuration(1000);
warAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
warCurValue1 = (float) valueAnimator.getAnimatedValue();
invalidate();
if (warCurValue1 == 1) {
warAnimator2.start();
}
}
});
warAnimator2 = ValueAnimator.ofFloat(0, 1);
warAnimator2.setDuration(1000);
warAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
warCurValue2 = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (viewStatue == Contants.ALIPLAY_FAILED) {
float circleStop = circlePathMeasure.getLength() * circleCurValue;
dstCirclePath.reset();
circlePathMeasure.getSegment(0, circleStop, dstCirclePath, true);
canvas.drawPath(dstCirclePath, paint);
float warStop1 = warCurValue1 * warPathMeasure1.getLength();
dstWarPath1.reset();
warPathMeasure1.getSegment(0, warStop1, dstWarPath1, true);
canvas.drawPath(dstWarPath1, paint);
float warStop2 = warCurValue2 * warPathMeasure2.getLength();
dstWarPath2.reset();
warPathMeasure2.getSegment(0, warStop2, dstWarPath2, true);
canvas.drawPath(dstWarPath2, paint);
}
}
注意
例子中有三个按钮,分别是加载按钮,成功按钮,失败按钮,需要注意的是,每次点击成功或失败按钮时候都需要先把成功动画进度和失败动画进度归零,不然二次点击时候,对号和错号都会直接显示之后再根据动画进度重绘。
public void success() {
if (trueAnimator != null) {
trueAnimator.cancel();
}
if (circleAnimator != null) {
circleAnimator.cancel();
}
trueCurValue = 0;
aniMator(Contants.ALIPLAY_SUCCESS);
}
public void failed() {
if (trueAnimator != null) {
trueAnimator.cancel();
}
if (circleAnimator != null) {
circleAnimator.cancel();
}
warCurValue1 = 0;
warCurValue2 = 0;
aniMator(Contants.ALIPLAY_FAILED);
}