手摸手教你写Slack的Loading动画

手摸手教你写Slack的Loading动画

项目地址:https://github.com/JeasonWong/SlackLoadingView

老规矩,先上效果。

说下第一眼看到这个动画后的思路:

  • 两根平行线,要用到直线方程 y=kx+b
  • 另外两根平行线,与之前两根平行线的斜率相乘为-1,即k1*k2=-1
  • 线条做圆周运动就是k值的不断变化
  • 然后就是简单的线条长度变化

我相信很多人第一眼会和我有类似的思路,但是当我上了个厕所后意识到我想复杂了~

说下上完厕所后的思路:

  • 不要想着线条是斜的,就是一个普通的线段,一个LineTo搞定(startX和stopX一样,仅Y不同)
  • 线条的垂直更容易,直接Canvas翻转(转过后再转回)
  • 整个动画的圆周运动也是Canvas翻转(转过后不转回)
  • 线条的单度变化依然用属性动画(这是必须的。。)
  • 动画开始前就让整个Canvas旋转

这样一来就太容易了。

我把动画分成了四步:

  • 画布旋转及线条变化动画(Canvas Rotate Line Change)
  • 画布旋转动画(Canvas Rotate)
  • 画布旋转圆圈变化动画(Canvas Rotate Circle Change)
  • 线条变化动画(Line Change)

详细说明前先介绍下成员变量和一些初始化

成员变量

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
33
34
35
36
//静止状态
private final int STATUS_STILL = 0;
//加载状态
private final int STATUS_LOADING = 1;
//线条最大长度
private final int MAX_LINE_LENGTH = dp2px(getContext(), 120);
//线条最短长度
private final int MIN_LINE_LENGTH = dp2px(getContext(), 40);
//最大间隔时长
private final int MAX_DURATION = 3000;
//最小间隔时长
private final int MIN_DURATION = 500;

private Paint mPaint;
private int[] mColors = new int[]{0xB07ECBDA, 0xB0E6A92C, 0xB0D6014D, 0xB05ABA94};
private int mWidth, mHeight;
//动画间隔时长
private int mDuration = MIN_DURATION;
//线条总长度
private int mEntireLineLength = MIN_LINE_LENGTH;
//圆半径
private int mCircleRadius;
//所有动画
private List<Animator> mAnimList = new ArrayList<>();
//Canvas起始旋转角度
private final int CANVAS_ROTATE_ANGLE = 60;
//动画当前状态
private int mStatus = STATUS_STILL;
//Canvas旋转角度
private int mCanvasAngle;
//线条长度
private float mLineLength;
//半圆Y轴位置
private float mCircleY;
//第几部动画
private int mStep;

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
private void initView() {
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setColor(mColors[0]);
}

private void initData() {
    mCanvasAngle = CANVAS_ROTATE_ANGLE;
    mLineLength = mEntireLineLength;
    mCircleRadius = mEntireLineLength / 5;
    mPaint.setStrokeWidth(mCircleRadius * 2);
    mStep = 0;
}

一、画布旋转及线条变化动画(Canvas Rotate Line Change)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
 * Animation1
 * 动画1
 * Canvas Rotate Line Change
 * 画布旋转及线条变化动画
 */
private void startCRLCAnim() {

    Collection<Animator> animList = new ArrayList<>();

    ValueAnimator canvasRotateAnim = ValueAnimator.ofInt(CANVAS_ROTATE_ANGLE + 0, CANVAS_ROTATE_ANGLE + 360);
    canvasRotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCanvasAngle = (int) animation.getAnimatedValue();
        }
    });

    animList.add(canvasRotateAnim);

    ValueAnimator lineWidthAnim = ValueAnimator.ofFloat(mEntireLineLength, -mEntireLineLength);
    lineWidthAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mLineLength = (float) animation.getAnimatedValue();
            invalidate();
        }
    });

    animList.add(lineWidthAnim);

    AnimatorSet animationSet = new AnimatorSet();
    animationSet.setDuration(mDuration);
    animationSet.playTogether(animList);
    animationSet.setInterpolator(new LinearInterpolator());
    animationSet.addListener(new AnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d("@=>", "动画1结束");
            if (mStatus == STATUS_LOADING) {
                mStep++;
                startCRAnim();
            }
        }
    });
    animationSet.start();

    mAnimList.add(animationSet);
}

第一步动画涉及到两个动画同时进行,所以使用了AnimatorSet,这个类很强大,可以让N个动画同时进行(playTogether),也可以让N个动画顺序执行(playSequentially)。

说到这里,其实我的四个动画就是顺序进行的,但是每个动画里又有同时进行的动画,为了讲解方便,我是监听了onAnimationEnd来控制动画执行顺序,其实可以直接使用playSequentially

上方动画就干了两件事:

1、旋转画布,从CANVAS_ROTATE_ANGLE + 0转到CANVAS_ROTATE_ANGLE + 360,CANVAS_ROTATE_ANGLE是画布初始倾斜角度

2、线条长度变化,从mEntireLineLength到-mEntireLineLength。

对应的onDraw方法:

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

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    switch (mStep % 4) {
        case 0:
            for (int i = 0; i < mColors.length; i++) {
                mPaint.setColor(mColors[i]);
                drawCRLC(canvas, mWidth / 2 - mEntireLineLength / 2.2f, mHeight / 2 - mLineLength, mWidth / 2 - mEntireLineLength / 2.2f, mHeight / 2 + mEntireLineLength, mPaint, mCanvasAngle + i * 90);
            }
            break;
        ...
    }

}

...

private void drawCRLC(Canvas canvas, float startX, float startY, float stopX, float stopY, @NonNull Paint paint, int rotate) {
    canvas.rotate(rotate, mWidth / 2, mHeight / 2);
    canvas.drawArc(new RectF(startX - mCircleRadius, startY - mCircleRadius, startX + mCircleRadius, startY + mCircleRadius), 180, 180, true, mPaint);
    canvas.drawLine(startX, startY, stopX, stopY, paint);
    canvas.drawArc(new RectF(stopX - mCircleRadius, stopY - mCircleRadius, stopX + mCircleRadius, stopY + mCircleRadius), 0, 180, true, mPaint);
    canvas.rotate(-rotate, mWidth / 2, mHeight / 2);
}

是不是很机智,drawCRLC做了三件事:

1、画布旋转后又旋转回来

2、画半圆(为什么要画半圆?不画整个圆?这里留个思考题。)

3、画线条

这样动画1就完成了。

二、画布旋转动画(Canvas Rotate)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
 * Animation2
 * 动画2
 * Canvas Rotate
 * 画布旋转动画
 */
private void startCRAnim() {
    ValueAnimator canvasRotateAnim = ValueAnimator.ofInt(mCanvasAngle, mCanvasAngle + 180);
    canvasRotateAnim.setDuration(mDuration / 2);
    canvasRotateAnim.setInterpolator(new LinearInterpolator());
    canvasRotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCanvasAngle = (int) animation.getAnimatedValue();
            invalidate();
        }
    });
    canvasRotateAnim.addListener(new AnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d("@=>", "动画2结束");
            if (mStatus == STATUS_LOADING) {
                mStep++;
                startCRCCAnim();
            }
        }
    });
    canvasRotateAnim.start();

    mAnimList.add(canvasRotateAnim);
}

...

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    switch (mStep % 4) {
        ...
        case 1:
            for (int i = 0; i < mColors.length; i++) {
                mPaint.setColor(mColors[i]);
                drawCR(canvas, mWidth / 2 - mEntireLineLength / 2.2f, mHeight / 2 + mEntireLineLength, mPaint, mCanvasAngle + i * 90);
            }
            break;
        ...
    }

}

...

private void drawCR(Canvas canvas, float x, float y, @NonNull Paint paint, int rotate) {
    canvas.rotate(rotate, mWidth / 2, mHeight / 2);
    canvas.drawCircle(x, y, mCircleRadius, paint);
    canvas.rotate(-rotate, mWidth / 2, mHeight / 2);
}

有了动画1的底子,那这个就太容易了,只是简单的旋转Canvas。

三、画布旋转圆圈变化动画(Canvas Rotate Circle Change)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
 * Animation3
 * 动画3
 * Canvas Rotate Circle Change
 * 画布旋转圆圈变化动画
 */
private void startCRCCAnim() {
    Collection<Animator> animList = new ArrayList<>();

    ValueAnimator canvasRotateAnim = ValueAnimator.ofInt(mCanvasAngle, mCanvasAngle + 90, mCanvasAngle + 180);
    canvasRotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCanvasAngle = (int) animation.getAnimatedValue();
        }
    });

    animList.add(canvasRotateAnim);

    ValueAnimator circleYAnim = ValueAnimator.ofFloat(mEntireLineLength, mEntireLineLength / 4, mEntireLineLength);
    circleYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCircleY = (float) animation.getAnimatedValue();
            invalidate();
        }
    });

    animList.add(circleYAnim);

    AnimatorSet animationSet = new AnimatorSet();
    animationSet.setDuration(mDuration);
    animationSet.playTogether(animList);
    animationSet.setInterpolator(new LinearInterpolator());
    animationSet.addListener(new AnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d("@=>", "动画3结束");
            if (mStatus == STATUS_LOADING) {
                mStep++;
                startLCAnim();
            }
        }
    });
    animationSet.start();

    mAnimList.add(animationSet);
}

...

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    switch (mStep % 4) {
        ...
        case 2:
            for (int i = 0; i < mColors.length; i++) {
                mPaint.setColor(mColors[i]);
                drawCRCC(canvas, mWidth / 2 - mEntireLineLength / 2.2f, mHeight / 2 + mCircleY, mPaint, mCanvasAngle + i * 90);
            }
            break;
        ...
    }

}

...

private void drawCRCC(Canvas canvas, float x, float y, @NonNull Paint paint, int rotate) {
    canvas.rotate(rotate, mWidth / 2, mHeight / 2);
    canvas.drawCircle(x, y, mCircleRadius, paint);
    canvas.rotate(-rotate, mWidth / 2, mHeight / 2);
}

动画3做了两件事:

1、旋转Canvas

2、变化Circle的Y坐标,达到往里缩的效果

四、线条变化动画(Line Change)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * Animation4
 * 动画4
 * Line Change
 * 线条变化动画
 */
private void startLCAnim() {
    ValueAnimator lineWidthAnim = ValueAnimator.ofFloat(mEntireLineLength, -mEntireLineLength);
    lineWidthAnim.setDuration(mDuration);
    lineWidthAnim.setInterpolator(new LinearInterpolator());
    lineWidthAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mLineLength = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
    lineWidthAnim.addListener(new AnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d("@=>", "动画4结束");
            if (mStatus == STATUS_LOADING) {
                mStep++;
                startCRLCAnim();
            }
        }
    });
    lineWidthAnim.start();

    mAnimList.add(lineWidthAnim);
}

...

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    switch (mStep % 4) {
        ...
        case 3:
            for (int i = 0; i < mColors.length; i++) {
                mPaint.setColor(mColors[i]);
                drawLC(canvas, mWidth / 2 - mEntireLineLength / 2.2f, mHeight / 2 + mEntireLineLength, mWidth / 2 - mEntireLineLength / 2.2f, mHeight / 2 + mLineLength, mPaint, mCanvasAngle + i * 90);
            }
            break;
    }

}

...

private void drawLC(Canvas canvas, float startX, float startY, float stopX, float stopY, @NonNull Paint paint, int rotate) {
    canvas.rotate(rotate, mWidth / 2, mHeight / 2);
    canvas.drawArc(new RectF(startX - mCircleRadius, startY - mCircleRadius, startX + mCircleRadius, startY + mCircleRadius), 0, 180, true, mPaint);
    canvas.drawLine(startX, startY, stopX, stopY, paint);
    canvas.drawArc(new RectF(stopX - mCircleRadius, stopY - mCircleRadius, stopX + mCircleRadius, stopY + mCircleRadius), 180, 180, true, mPaint);
    canvas.rotate(-rotate, mWidth / 2, mHeight / 2);
}

动画4只做了线条的变化。

这样整个Slack的Loading动画就完成了,是不是很简单。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值