盒子
盒子
文章目录
  1. 前言
  2. 思路拆分
  3. 圆弧的实现
  4. Icon的缩放、放大与透明
    1. 放大透明
    2. 缩放透明
  5. 动画调用
    1. 开启
    2. 关闭

一个loading动画的全面解析

前言

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

loading

看起来没有什么特别炫的动画,因为这不是今天正在的主题,我们真正要认识的是动画实现的思想,通过一个相对较简洁的动画,来举一反三,以后类似的动画也不成什么问题了。

思路拆分

首先第一眼看到这个动画,你脑海里想的是什么呢?可能有些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);
}

对于自定义的实现,我们一般都要实现onMeasureonDraw方法,其中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来设置不同的大小,如果modeEXACTLY即为精确的值,我们就使用控件所定义的大小,否则就使用默认设置的大小。在此同时创建一个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

支持一下
赞赏是一门艺术