前言
今天来聊聊关于动画的一点自己的心得,在此对需要注意的地方做下总结,也希望帮助大家少走一点弯路。先来看下今天的动画效果吧。

看起来没有什么特别炫的动画,因为这不是今天正在的主题,我们真正要认识的是动画实现的思想,通过一个相对较简洁的动画,来举一反三,以后类似的动画也不成什么问题了。
思路拆分
首先第一眼看到这个动画,你脑海里想的是什么呢?可能有些people
会说一个gif
图就能简单的解决。是的,这也是一种实现方式,但做为一个对于技术的追求者,我还是认为应该自己通过原生来实现,这样对自己也是一种很大提升。好了,下面我对这动画进行拆分,对问题进行拆分是一个很有效的方法。它能让我们的思路清晰,从而深刻的认识到问题的解决方式。通过仔细的观察,我们可以将动画拆除四部分
- 先画圆弧
- 在画圆弧的过程中,对中心部分缩放透明
- 圆弧画完之后有一段时间停顿形成完整的圆
- 最后在停顿的时刻再对中心部分放大透明
以上是动画的拆分,现在我们对动画有了清晰的了解之后,我们又可以对实现的动画部分进行拆分,我们可以观察到,其实整体可以看成两个动画的合成
- 圆弧的绘制,与画完后的圆的定形
- 中心
Icon
的缩放、放大与透明变化
所以接下来我们对这两大部分进行分布实现
圆弧的实现
首先是对圆弧的实现,经过上面的分析,我们要对圆弧进行动态绘制,完成圆后进行有限时间的定形,最后就是循环以上步骤而已。首先自定义LoadingCirclerView
继承与ImageView
。既然要自己实现动画的绘制,自然少不了对Paint
的使用,所以首先初始化画笔。
1 2 3 4 5 6 7
| private void init() { mPaint = new Paint(); mPaint.setColor(getResources().getColor(R.color.black)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mCircleWidth); mPaint.setAntiAlias(true); }
|
对于自定义的实现,我们一般都要实现onMeasure
与onDraw
方法,其中onMeasure
方法是对我们自定义View
的大小进行一个测量,而onDraw
方法自然就是核心部分的绘制逻辑。这里要注意的是在onDraw
中不要来创建对象与实现由延时的操作,因为onDraw
是非常频繁调用的方法,如果其中有对象的创建会加速应用的内存的增加,导致OOM
。所以对于上面的Paint
的初始化都是单独抽出来。下面来看下onMeasure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { mWidth = width; } else { mWidth = DisplayUtils.dip2px(DEFAULT_SIZE); } if (heightMode == MeasureSpec.EXACTLY) { mHeight = height; } else { mHeight = DisplayUtils.dip2px(DEFAULT_SIZE); } //百分比弧矩形 mRectF = new RectF(mCircleWidth, mCircleWidth, mWidth - mCircleWidth, mHeight - mCircleWidth); setMeasuredDimension(mWidth, mHeight); }
|
这里我们主要是通过不同的mode
来设置不同的大小,如果mode
为EXACTLY
即为精确的值,我们就使用控件所定义的大小,否则就使用默认设置的大小。在此同时创建一个RectF
为后续的画圆弧进行相关数据的配置。最后一定要调用setMeasuredDimension
方法来设置我们所设置的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Override protected void onDraw(Canvas canvas) { if (isStart) { if (mCirclePercent <= 100) { //开始画圆弧 isRestart = true; canvas.drawArc(mRectF, 270, -mCirclePercent / 100.f * 360, false, mPaint); mCirclePercent += mCirclerPercentIncremental; } else if (isRestart) { //是否重新开始画圆弧 //周期性完成的回调 if (listener != null) { listener.onFinish(); } isRestart = false; //重置数据 setReset(); //绘制完整的圆 canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2 - mCircleWidth, mPaint); //显示完整圆的时间 postInvalidateDelayed(CIRCLE_FIXED_TIME); } if (isStart && isRestart) { //定时更新ui postInvalidateDelayed(mIntervalTime); mIntervalTime -= mDrawIntervalTimeTDecrement; } } else { //绘制完整的圆 canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2 - mCircleWidth, mPaint); } super.onDraw(canvas); }
|
onDraw
方法就是整个动画的逻辑,通过上面的动画效果图,发现动画的开始是从270
度的角度进行绘制的,然后再逆时针的完成圆的绘制。所以在画圆弧的时候,它的初始角度就是270
,然后就是动态的增加绘制的角度,调用canvas.drawArc
方法来绘制圆弧,再通过postInvalidateDelayed
方法来达到定时的更新View
的绘制,以此来实现动画的效果。以上就是画圆弧的整个过程,这样就完成了一半,下面进行下一半。
Icon的缩放、放大与透明
Icon
的动画也可以分成两步,缩放透明与放大透明,我们发现没一次Icon
动画的实现都伴有透明的实现,所以我们要实现这两小步都要使用到动画的集合性质。对动画进行叠加。
放大透明
既然要用到集合,可以使用AnimationSet
来管理
1 2 3 4 5 6 7 8 9
| //放大透明 AlphaAnimation alphaAnimation1 = new AlphaAnimation(0.2f, 1.0f); alphaAnimation1.setDuration(550); mSetAnimation1.addAnimation(alphaAnimation1);
ScaleAnimation scaleAnimation2 = new ScaleAnimation(1.05f, 1.0f, 1.05f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); scaleAnimation2.setDuration(550); mSetAnimation1.addAnimation(scaleAnimation2); mSetAnimation1.setFillAfter(true);
|
mSetAnimation1.setFillAfter(true);
是关键,它代表动画结束后保持结束的视图效果。其它就是api
的使用了,这里就不多说了。
缩放透明
1 2 3 4 5 6 7 8 9 10
| //缩放透明 mSetAnimation2 = new AnimationSet(true); AlphaAnimation alphaAnimation2 = new AlphaAnimation(1.0f, 0.2f); alphaAnimation2.setDuration(100); ScaleAnimation scaleAnimation3 = new ScaleAnimation(1.0f, 0.6f, 1.0f, 0.6f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); scaleAnimation3.setDuration(100);
mSetAnimation2.addAnimation(alphaAnimation2); mSetAnimation2.addAnimation(scaleAnimation3); mSetAnimation2.setFillAfter(true);
|
以上还有一个要注意的地方,他们的时间都不是随便设置的,因为他们又要结合上面的画圆弧的动画,所以时间都是通过计算设计的。所以要改时间要两部分相对于的修改。
至此动画都基本完成了,思路是否很清晰呢。下面剩下的就是动画的调用了。
动画调用
开启
首先是动画的开启
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public void show() { //1.重置,开启动画 mLoadingImage.setVisibility(VISIBLE); mLoadingCircleView.setVisibility(VISIBLE); mLoadingCircleView.setReset(); mLoadingCircleView.startAnimation(); mLoadingCircleView.invalidate(); //2.缩放动画推辞200毫秒执行 mLoadingImage.postDelayed(new Runnable() { @Override public void run() { mLoadingImage.startAnimation(mSetAnimation2); } }, 200); }
|
这里的mLoadingCircleVieW
就是我们自定义的圆弧控件。mLoadingImage
就是Icon
它就是一个ImageView
。首先我们开启对圆弧的绘制,同时推辞200
毫秒再对Icon
进行缩放透明的动画进行调用。当圆弧画完之后,经过前面的onDraw
方法,我们会发现它有1200
毫秒的整个圆的展示,同时通过onFinish
回调,我们可以进行Icon
的放大透明动画。
1 2 3 4 5 6
| @Override public void onFinish() { //3.圆形画完后立刻执行放大透明动画 mLoadingImage.setVisibility(VISIBLE); mLoadingImage.startAnimation(mSetAnimation1); }
|
到这里就是这个动画的一个完整的周期。因为要实现循环,所以这里还差最后一步,就是将动画还原到初始的状态。我们可以通过对第三步放大透明的动画进行监听,当放大透明的动画结束后,再进行缩放透明还原。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| scaleAnimation2.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {
} @Override public void onAnimationEnd(Animation animation) { //4:一个周期动画结束后,执行缩放透明,返回到初始化状态, // 这里延迟650毫秒,因为固定圆形显示时间为1200毫秒 需再减去前面的放大透明动画的执行时间550毫秒 mSetAnimation2.setStartOffset(650); mLoadingImage.startAnimation(mSetAnimation2); } @Override public void onAnimationRepeat(Animation animation) { } });
|
关闭
1 2 3 4 5 6 7 8 9 10
| public void dismiss() { mLoadingCircleView.stopAnimation(); mLoadingCircleView.setVisibility(GONE); mLoadingImage.setVisibility(GONE); //清理动画,防止View无法隐藏 //因为View的类型动画是对View的镜像进行动画,并没有改变View的状态 if (mLoadingImage.getAnimation() != null) { mLoadingImage.clearAnimation(); } }
|
对于动画的关闭我要说的都在上面的代码注释进行了详细的说明。只要注意这一点就基本没有问题了。
OK,everything is over. 最后附上源码地址
源代码:loading