android 自定义view仿支付宝支付结果动画

本文记录一下仿支付宝支付结果动画过程。
效果如下:
在这里插入图片描述在这里插入图片描述

首先,实现一个首尾互相追赶,忽长忽短的动画过程。 (与google的等待动画相似)
这里通过canvas的drawArc方式实现, 该方法签名如下:

public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
	super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
}

参数中有起始角度和扫过角度, 我们可以通过改变起始角度 startAngle 和 扫过的角度 sweepAngle 来实现这个动画的过程。

故我们用两个属性动画分别控制这两个变量,代码如下:

/**
* 开始加载动画
*/
public void startLoadingAnimation() {
	Log.i(TAG, "startLoadingAnimation: ");
	mStatus = Status.LOADING;
	mStartAngleAnimator = ValueAnimator.ofFloat(0, 360);
	mStartAngleAnimator.setDuration(2000);
	mStartAngleAnimator.setRepeatCount(ValueAnimator.INFINITE);
	mStartAngleAnimator.setInterpolator(ANGLE_INTERPOLATOR);
	mStartAngleAnimator.setRepeatMode(ValueAnimator.RESTART);
	mStartAngleAnimator.addUpdateListener(animation -> {
	    mCurrentGlobalAngle = (float) animation.getAnimatedValue();
	    invalidate();
	});
	mStartAngleAnimator.start();
	
	// mCurrentSweepAngle 从 0 到 300 变化
	mSweepAngleAnimator = ValueAnimator.ofFloat(0, 360 - 2 * MIN_SWEEP_ANGLE);
	mSweepAngleAnimator.setDuration(600);
	mSweepAngleAnimator.setRepeatCount(ValueAnimator.INFINITE);
	mSweepAngleAnimator.setInterpolator(SWEEP_INTERPOLATOR);
	mSweepAngleAnimator.setRepeatMode(ValueAnimator.RESTART);
	mSweepAngleAnimator.addUpdateListener(animation -> {
	    mCurrentSweepAngle = (float) animation.getAnimatedValue();
	    invalidate();
	});
	mSweepAngleAnimator.addListener(new AnimatorListenerAdapter() {
	    @Override
	    public void onAnimationRepeat(Animator animation) {
	        super.onAnimationRepeat(animation);
	        // 重复动画时修正 mCurrentGlobalAngle 开始角度的。 
	        toggleAppearingMode();
	    }
	});
	mSweepAngleAnimator.start();
}

根据这两个值来绘制圆弧,代码如下:

if (mStatus == Status.LOADING) {
    float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset;
    float sweepAngle = mCurrentSweepAngle;
    if (!mModeAppearing) {
    	// 圆弧变短的过程, startAngle 累加上 sweepAngle 形成一种旋转的假像
        startAngle += sweepAngle;
        // mCurrentSweepAngle从 0 到 300 变化,故这里 sweepAngle 会减小
        sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE;
    } else {
    	// 圆弧变长的过程
        sweepAngle += MIN_SWEEP_ANGLE;
    }
    canvas.drawArc(mOval, startAngle, sweepAngle, false, mPaint);
    // 保存相关值,作为后面画整圆的起始值
    mCurrentGlobalAngle = startAngle;
    mCurrentSweepAngle = sweepAngle;
}

如上代码通过 sweepAngle 与 sweepAngle累加形成旋转的动画,而 sweepAngle角度控制弧长忽长忽短。 其中
通过 float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset; 方式来计算 startAngle 是为了修正动画一次后,将 startAngle 停留在上一次结束的位置,而不是从0重新开始(这样会造成动画抖动不连贯)。
mCurrentGlobalAngleOffset的值计算方法如下:

/**
* 如果完成一次扫的动画,就要重新计算开始角度的偏移,以防动画抖动不连贯
 */
private void toggleAppearingMode() {
    Log.i(TAG, "toggleAppearingMode: ");
    mModeAppearing = !mModeAppearing;
    if (mModeAppearing) {
        mCurrentGlobalAngleOffset = (mCurrentGlobalAngleOffset + 2 * MIN_SWEEP_ANGLE) % 360;
    }
}

因为 mCurrentGlobalAngle 动画一次,转过的角度为360 度,并且还要累加扫过的300度。故弧长变长的一次动画过程,mCurrentGlobalAngle 转过的角度为 360 + 300,也即-60度的位置。
故 mCurrentGlobalAngle 再次动画时,起始值为0,减去 mCurrentGlobalAngleOffset 为 60 的整数倍,(每次动画停止的位置不同,但分析类同)正好停在上一次动画结束的位置,这样就让动画衔接起来了。

其次,再将上面的圆弧变成整圆。 由于之前的等待框在弧长最长时还有30度的弧未闭合,故先执行一个过度的动画,让圆圈以动画的形式闭合,然后再来绘制对勾 或 叉。

动画部分代码如下:

/**
 * 将有缺口的圆圈画完整,然后再画出相应的状态
 */
public void startWholeCircleAnimation(AnimatorListenerAdapter adapter) {
    mStatus = Status.MIDDLE;
    Log.i(TAG, "startWholeCircleAnimation: ");
    if (mSweepAngleAnimator != null && mSweepAngleAnimator.isRunning()) {
        mSweepAngleAnimator.cancel();
    }

    if (mStartAngleAnimator != null && mStartAngleAnimator.isRunning()) {
        mStartAngleAnimator.cancel();
    }

    mStartAngleAnimator = ValueAnimator.ofFloat(mCurrentGlobalAngle % 360, 360);
    mStartAngleAnimator.setDuration(2000);
    mStartAngleAnimator.setRepeatCount(ValueAnimator.INFINITE);
    mStartAngleAnimator.setInterpolator(ANGLE_INTERPOLATOR);
    mStartAngleAnimator.setRepeatMode(ValueAnimator.RESTART);
    mStartAngleAnimator.addUpdateListener(animation -> {
        mCurrentGlobalAngle = (float) animation.getAnimatedValue();
        invalidate();
    });
    mStartAngleAnimator.start();

    // 只需将扫过的角度调整为360度即可。
    mSweepAngleAnimator = ValueAnimator.ofFloat(mCurrentSweepAngle % 360, 360);
    mSweepAngleAnimator.setDuration(600);
    mSweepAngleAnimator.setInterpolator(SWEEP_INTERPOLATOR);
    mSweepAngleAnimator.addUpdateListener(animation -> {
        mCurrentSweepAngle = (float) animation.getAnimatedValue();
        invalidate();
    });
    mSweepAngleAnimator.addListener(adapter);
    mSweepAngleAnimator.start();
}

这两个动画,分别以上一次结束动画时的 mCurrentGlobalAngle 和 mCurrentSweepAngle 为起始值,均动画到360度。
对应的绘制代码如下:

if (mStatus == Status.MIDDLE) {
    float startAngle = mCurrentGlobalAngle;
    float sweepAngle = mCurrentSweepAngle;

    canvas.drawArc(mOval, startAngle, sweepAngle, false, mPaint);
}

至此,就绘制了一个整圆了。

然后,分别绘制对勾 和叉,这里使用的是的path结合pathmeasure来实现。 代码如下:

/**
 * 构建对勾的path,画的比较随意
 */
private void initSuccessPath() {
    Log.i(TAG, "initSuccessPath: ");
    // 主要是画一个对勾的形状
    mSuccPath.moveTo(mCenterX - mInnerRectSize / 2, mCenterY);
    mSuccPath.lineTo(mCenterX, mCenterY + mInnerRectSize / 2);
    mSuccPath.lineTo(mCenterX + mInnerRectSize / 2, mCenterY);

    // 整体向上移动一点,好看一些
    mSuccPath.offset(0, -mInnerRectSize / 4);

    // 先克隆一个path用来计算目标path有多少个轮廓。 
    Path tmpPath = new Path(mSuccPath);
    // 起始是 1
    int pathCount = 1;
    PathMeasure pm = new PathMeasure(tmpPath, false);
    // 此种情形的path由于是连续的,只有1条轮廓。 (后面错误的path有两条轮廓)
    while (pm.nextContour()) {
        pathCount++;
    }
    startSuccAnimation(pathCount);
}

private void startSuccAnimation(int pathCount) {
    Log.i(TAG, "startSuccAnimation: ");
    mStatus = Status.SUCC;
    mDst.reset();
    PathMeasure measure = new PathMeasure(mSuccPath, false);
    ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.0f);
    va.setDuration(600);
    va.setInterpolator(new LinearInterpolator());
    va.setRepeatMode(ValueAnimator.RESTART);
    // 有几个轮廓就执行几次动画
    va.setRepeatCount(pathCount);

    va.addUpdateListener(animation -> {
        float curVal = (float) animation.getAnimatedValue();
        measure.getSegment(0, curVal * measure.getLength(), mDst, true);
        invalidate();
    });

    va.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationRepeat(Animator animation) {
            super.onAnimationRepeat(animation);
            // 切换该path的下一个轮廓。 
            measure.nextContour();
        }
    });
    va.start();
}

对于错误状态的处理也同上,只是构建错误path路径不一样。具体代码见文后完整代码。
对于这两种状态的绘制代码比较简单,如下:

if (mStatus == Status.SUCC) {
    canvas.drawArc(mOval, 0, 360, false, mPaint);
    canvas.drawPath(mDst, mPaint);
} else if (mStatus == Status.FAIL) {
    canvas.drawArc(mOval, 0, 360, false, mPaint);
    canvas.drawPath(mDst, mPaint);
}

完成之后,对外暴露一个设置状态的接口即可。
本文只是记录实现过程中的一些想法,文中代码只截取了部分,如果不慎点进来可以结合后文完整代码查看。

注意事项:
1、ValueAnimator 默认的插值器为 AccelerateDecelerateInterpolator , 在某些不断重复的旋转过程中,如果不设置线性插值器,可能会有卡顿一下的感觉。
2、在用 pathmeasure 计算轮廓数量时,最好克隆一个path。因为 调用nextContour后会切换到下一条轮廓,可能会导致绘制不正确。
3、在使用 measure.getSegment 截取path片断时,会将结果add 进 mDstPath,故在适当时候要reset一下。
4、上面等待动画计算有点不好理解,可以使用只动态改变扫过的角度,同时加入旋转来实现此动画。
5、path有个offset方法可以用来移动path的位置。

参考:
1、circular-progress-button

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值