Android自定义View之app更新动画

本篇文章是实现项目中的更新功能,效果如下

Picture

观察动画,可以分为几个阶段:

  • 初始化阶段 显示立即升级按钮,在点击立即升级按钮后,执行放大再缩小至消失动画

  • 准备阶段 进度条背景从中间向两端扩散,然后进度提示图片显示,进度提示文字显示0%

  • 更新阶段 进度更新时,进度提示图片和文字旋转向前移动,如果一定时间内进度没更新的话,进度提示图片和文字要置回水平状态

  • 成功阶段,进度提示图片缩放消失,进度条背景从两端向中间缩小至消失

  • 安装阶段 马上安装图片放大显示

1.首选看初始化阶段,我们要判断用户是否点击了立即升级按钮,我们通过监听onTouchEvent事件判断手指是否落在可点击区域

//如果点击生效,执行动画

if (rectClickRange.contains(event.getX(), event.getY()))
startBtnDisappear();//立即更新按钮消失动画

其中rectClickRange是我们定义的可点击区域,也就是立即更新图片的显示区域

rectClickRange = new RectF(getWidth() / 2 - startDrawable.getWidth() / 2, getHeight() / 2 - startDrawable.getHeight() / 2, getWidth() / 2 + startDrawable.getWidth() / 2, getHeight() / 2 + startDrawable.getHeight() / 2);//startDrawable是立即更新图片

点击生效后我们执行立即更新按钮消失动画,代码如下

/**
 * 点击立即升级的时候,立即升级按钮执行消失动画
 * 动画效果是按钮放大一点之后缩小至消失
 * 根据效果选择插值器AnticipateInterpolator(开始的时候向后然后向前甩)
 * 将bitmapscale设置到立即升级图片上
 * 动画结束后状态更新为准备状态
 */

private void startBtnDisappear() {
    initStateData();
    ValueAnimator va = ValueAnimator.ofInt(0, 1);
    va.setInterpolator(new AnticipateInterpolator());
    va.setDuration(800);
    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            bitmapScale = 1 - animation.getAnimatedFraction();
            invalidate();
        }
    });
    va.addListener(new MyAnimationListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            cancleValueAnimator(va_List);
            state = PREPARE;
            toPrepare();
        }
    });
    va.start();
    va_List.add(va);
}

然后在onDraw里面绘制立即升级按钮动画,代码如下:


                matrix.reset();
                matrix.setScale(bitmapScale, bitmapScale);//缩放图片
                matrix.preTranslate(0, 0);
                matrix.postTranslate(width / 2 - startDrawable.getWidth() / 2 * bitmapScale, height / 2 - startDrawable.getHeight() / 2 * bitmapScale);//不断的改变缩放的中心点
                canvas.drawBitmap(startDrawable, matrix, bitmapPaint);

2.接着我们看一下准备阶段,我们通过画path,并不断的改变path的起点和终点达到所需要的动画效果,代码如下:

/**
 * PREPARE状态
 * 进度条从中间向两端扩散
 * 具体做法是不断改变path的起点和终点坐标
 * 动画结束的时候开始下载更新
 */

private void toPrepare() {
    final ValueAnimator va = ValueAnimator.ofFloat(0, width / 2 - pbPaint.getStrokeWidth() * 3 - pbProgerssDrawable.getWidth());
    va.setInterpolator(new LinearInterpolator());
    va.setDuration(200);
    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float value = (Float) animation.getAnimatedValue();
            startX = (int) (width / 2 - value);
            endX = (int) (width / 2 + value);
            if (animation.getAnimatedFraction() == 1) prepareDone = true;
            invalidate();
        }
    });
    va.addListener(new MyAnimationListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            if (startDownLoadListener != null && !isSetListener) {
                isSetListener = true;
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        state = UPDATEING;                 
                        startDownLoadListener.downLoad();//动画结束,通知界面开始下载apk
                        text = progress * 100 / max + "%";
                    }
                }, 200);
            }
        }
    });
    va.start();
    va_List.add(va);
}

具体的绘制代码如下


                pbPath.reset();
                pbPath.moveTo(startX, height / 2);
                pbPath.lineTo(endX, height / 2);
                canvas.drawPath(pbPath, pbPaint);//绘制path
                //进度条完全显示后,画进度提示图片和文字
                if (prepareDone) {
                    canvas.drawBitmap(pbProgerssDrawable, startX - pbProgerssDrawable.getWidth() / 2, height / 2 - pbProgerssDrawable.getHeight() - pbPaint.getStrokeWidth(), bitmapPaint);
                    String text = "0%";
                    textPaint.getTextBounds(text.toCharArray(), 0, text.toCharArray().length, textRect);
                    canvas.drawText(text, startX - textRect.right / 2, height / 2 - pbProgerssDrawable.getHeight() / 2 - pbPaint.getStrokeWidth() + textRect.bottom, textPaint);
                }

3.这个时候界面就开始下载apk(代码不贴了),然后通知view来更新进度,更新的动画是图片和文字旋转向前移动(我们的做法是将画布旋转),如果一定时间进度没有变化,更新的图片和文字置回正常状态(我们通过启动线程不断的将画布旋转回来并更新view,如果这个阶段进度有更新的话,我们把线程remove掉),绘制代码如下

            pbPath.reset();
            pbPath.moveTo(startX, height / 2);
            pbPath.lineTo(endX, height / 2);
            pm.setPath(pbPath, false);
            //不断截取进度到pbPathSec并绘制
            if (progressOffsetX >= pm.getLength()) {
                pm.getSegment(0, pm.getLength(), pbPathSec, true);
                pm.getPosTan(pm.getLength(), POS, null);
            } else {
                pm.getSegment(0, progressOffsetX, pbPathSec, true);
                pm.getPosTan(progressOffsetX, POS, null);
            }
            matrix.reset();
            matrix.setTranslate(POS[0] - pbProgerssDrawable.getWidth() / 2, POS[1] - pbProgerssDrawable.getHeight() - pbPaint.getStrokeWidth());
            canvas.drawPath(pbPath, pbPaint);
            canvas.drawPath(pbPathSec, pbUpdatePaint);
            canvas.save();
            //如果进度没有到达100%,并且进度在更新的时候,画布旋转,然后画进度提示图片和文字
            if (progressOffsetX < pm.getLength() &amp;&amp; !isRotate) {
                canvas.rotate(-15, POS[0], POS[1] - pbPaint.getStrokeWidth() / 2);
            }
            canvas.drawBitmap(pbProgerssDrawable, matrix, bitmapPaint);
            if (progressOffsetX >= pm.getLength())
                progressOffsetX = pm.getLength();
            text = (int) (progressOffsetX * 100 / pm.getLength()) + "%";
            textPaint.getTextBounds(text.toCharArray(), 0, text.toCharArray().length, textRect);
            canvas.drawText(text, progressOffsetX + startX - textRect.right / 2, height / 2 - pbProgerssDrawable.getHeight() / 2 - pbPaint.getStrokeWidth() + textRect.bottom, textPaint);
            //我们启动一个线程,如果300毫秒进度没有更新,将画布旋转回来画进度提示图片和文字
            if (progressOffsetX < pm.getLength()) postDelayed(rotateRunnable, 300);
            else toSucc();
            canvas.restore();

其中rotateRunnable的代码如下



    //每隔一段时间刷新界面,如果进度没有更新,将画布旋转回来
    private Runnable rotateRunnable = new Runnable() {
        @Override
        public void run() {
            isRotate = true;
            invalidate();
        }
    };

4.当进度达到100%的时候,我们将进度提示图片缩放至消失,并且进度背景执行两端向中间缩小动画,也是改变path的起始点,代码如下

 //下载进度达到100时,进度提示图片进行缩放
    private void toSuccBitmapScale() {
        cancleValueAnimator(va_List);
        ValueAnimator va = ValueAnimator.ofFloat(1, 0);
        va.setInterpolator(new AccelerateInterpolator());
        va.setDuration(100);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                bitmapScale = (Float) animation.getAnimatedValue();
                state = SUCCESS;
                invalidate();
            }
        });
        va.start();
        va.addListener(new MyAnimationListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                toSuccPathAnim();
            }
        });
        va_List.add(va);
    }

    //成功后进度条缩放动画
    private void toSuccPathAnim() {
        cancleValueAnimator(va_List);
        ValueAnimator va = ValueAnimator.ofInt(0, (endX - startX) / 2);
        va.setInterpolator(new AccelerateInterpolator());
        va.setDuration(300);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                transx = (int) animation.getAnimatedValue();
                state = SUCCESS;
                invalidate();
            }
        });
        va.start();
        va.addListener(new MyAnimationListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                toInstall();
            }
        });
        va_List.add(va);
    }

绘制代码如下

            pbPath.reset();
            pbPath.moveTo(startX + transx, height / 2);//不断的改变起点
            pbPath.lineTo(endX - transx, height / 2);//改变终点
            pm.setPath(pbPath, false);
            pm.getSegment(0, (endX - startX), pbPathSec, true);
            pm.getPosTan(endX - startX, POS, null);
            matrix.reset();
            matrix.preTranslate(POS[0] - pbProgerssDrawable.getWidth() / 2, POS[1] - pbProgerssDrawable.getHeight() - pbPaint.getStrokeWidth());
            matrix.postScale(bitmapScale, bitmapScale, POS[0], POS[1] - pbPaint.getStrokeWidth());
            canvas.drawPath(pbPath, pbUpdatePaint);//path缩放动画
            canvas.drawBitmap(pbProgerssDrawable, matrix, bitmapPaint);//bitmap缩放动画

5.最后就是显示马上安装图片动画了,一个简单的缩放


  //显示马上安装图片动画
    private void toInstall() {
        cancleValueAnimator(va_List);
        ValueAnimator va = ValueAnimator.ofInt(0, 1);
        va.setInterpolator(new LinearInterpolator());
        va.setDuration(400);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                bitmapScale = animation.getAnimatedFraction();
                state = INSTALL;
                invalidate();
            }
        });
        va.start();
        va_List.add(va);
    }


      //绘制代码如下
                matrix.reset();
                matrix.setScale(bitmapScale, bitmapScale);
                matrix.preTranslate(0, 0);
                matrix.postTranslate(width / 2 - succDrawable.getWidth() / 2 * bitmapScale, height / 2 - succDrawable.getHeight() / 2 * bitmapScale);
                canvas.drawBitmap(succDrawable, matrix, bitmapPaint);

回过头来看看,其实当我们把动画不断的分解之后,发现其实每个动画并没有那么难,我们这里用到的有path绘制及截取,getPosTan(获取路径上某点的坐标及其切线的坐标),利用Matrix做动画,使用属性动画ValueAnimator。本篇还有好多功能没有实现,比如下载失败动画,失败后恢复至初始化动画,不过任何轮子都不一定能完全适合你,学习到知识之后自己造一个适合自己的才是最重要。

好了,本篇代码已经上传github,欢迎star
github地址:https://github.com/MrAllRight/BezierView

猜你喜欢

转载自blog.csdn.net/haoxuhong/article/details/81010251