一步步教你寫Slack的Loading動畫
項目地址:https://github.com/JeasonWong/SlackLoadingView
老規(guī)矩,先上效果。

圖好大。。
說下第一眼看到這個動畫后的思路:
+兩根平行線,要用到直線方程 y=kx+b
+另外兩根平行線,與之前兩根平行線的斜率相乘為-1,即k1*k2=-1
+線條做圓周運動就是k值的不斷變化
+然后就是簡單的線條長度變化
我相信很多人第一眼會和我有類似的思路,但是當(dāng)我上了個廁所后意識到我想復(fù)雜了~
說下上完廁所后的思路:
不要想著線條是斜的,就是一個普通的線段,一個LineTo搞定(startX和stopX一樣,僅Y不同)
線條的垂直更容易,直接Canvas翻轉(zhuǎn)(轉(zhuǎn)過后再轉(zhuǎn)回)
整個動畫的圓周運動也是Canvas翻轉(zhuǎn)(轉(zhuǎn)過后不轉(zhuǎn)回)
線條的單度變化依然用屬性動畫(這是必須的。。)
動畫開始前就讓整個Canvas旋轉(zhuǎn)
這樣一來就太容易了。
我把動畫分成了四步:
畫布旋轉(zhuǎn)及線條變化動畫(Canvas Rotate Line Change)
畫布旋轉(zhuǎn)動畫(Canvas Rotate)
畫布旋轉(zhuǎn)圓圈變化動畫(Canvas Rotate Circle Change)
線條變化動畫(Line Change)
詳細說明前先介紹下成員變量和一些初始化
成員變量
//靜止?fàn)顟B(tài)
private final int STATUS_STILL = 0;
//加載狀態(tài)
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起始旋轉(zhuǎn)角度
private final int CANVAS_ROTATE_ANGLE = 60;
//動畫當(dāng)前狀態(tài)
private int mStatus = STATUS_STILL;
//Canvas旋轉(zhuǎn)角度
private int mCanvasAngle;
//線條長度
private float mLineLength;
//半圓Y軸位置
private float mCircleY;
//第幾部動畫
private int mStep;
初始化
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;
}
一、畫布旋轉(zhuǎn)及線條變化動畫(Canvas Rotate Line Change)
/**
* Animation1
* 動畫1
* Canvas Rotate Line Change
* 畫布旋轉(zhuǎn)及線條變化動畫
*/
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結(jié)束");
if (mStatus == STATUS_LOADING) {
mStep++;
startCRAnim();
}
}
});
animationSet.start();
mAnimList.add(animationSet);
}
第一步動畫涉及到兩個動畫同時進行,所以使用了AnimatorSet,這個類很強大,可以讓N個動畫同時進行(playTogether),也可以讓N個動畫順序執(zhí)行(playSequentially)。
說到這里,其實我的四個動畫就是順序進行的,但是每個動畫里又有同時進行的動畫,為了講解方便,我是監(jiān)聽了onAnimationEnd來控制動畫執(zhí)行順序,其實可以直接使用playSequentially。
上方動畫就干了兩件事:
1、旋轉(zhuǎn)畫布,從CANVAS_ROTATE_ANGLE + 0轉(zhuǎn)到CANVAS_ROTATE_ANGLE + 360,CANVAS_ROTATE_ANGLE是畫布初始傾斜角度
2、線條長度變化,從mEntireLineLength到-mEntireLineLength。
對應(yīng)的onDraw方法:
@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、畫布旋轉(zhuǎn)后又旋轉(zhuǎn)回來
2、畫半圓(為什么要畫半圓?不畫整個圓?這里留個思考題。)
3、畫線條
這樣動畫1就完成了。
二、畫布旋轉(zhuǎn)動畫(Canvas Rotate)
/**
* Animation2
* 動畫2
* Canvas Rotate
* 畫布旋轉(zhuǎn)動畫
*/
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結(jié)束");
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的底子,那這個就太容易了,只是簡單的旋轉(zhuǎn)Canvas。
三、畫布旋轉(zhuǎn)圓圈變化動畫(Canvas Rotate Circle Change)
/**
* Animation3
* 動畫3
* Canvas Rotate Circle Change
* 畫布旋轉(zhuǎn)圓圈變化動畫
*/
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結(jié)束");
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、旋轉(zhuǎn)Canvas
2、變化Circle的Y坐標(biāo),達到往里縮的效果
四、線條變化動畫(Line Change)
/**
* 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結(jié)束");
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動畫就完成了,是不是很簡單。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- iOS動畫教你編寫Slack的Loading動畫進階篇
- Android自定義加載loading view動畫組件
- Android實現(xiàn)創(chuàng)意LoadingView動畫效果
- 一看就喜歡的loading動畫效果Android分析實現(xiàn)
- jQuery實現(xiàn)彩帶延伸效果的網(wǎng)頁加載條loading動畫
- 三款A(yù)ndroid炫酷Loading動畫組件推薦
- Winform圓形環(huán)繞的Loading動畫實現(xiàn)代碼
- javascript制作loading動畫效果 loading效果
- javascript 通用loading動畫效果實例代碼
- loading動畫特效小結(jié)
相關(guān)文章
Android仿微信Viewpager-Fragment惰性加載(lazy-loading)
這篇文章主要為大家詳細介紹了Android仿微信Viewpager-Fragment惰性加載lazy-loading,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
Mac Android Studio 3.0 Terminal 中文亂碼問題處理
本文給大家分享的是在更新Android Studio 3.0之后,使用Terminal時,發(fā)現(xiàn) git log 命令查看歷史 log會亂碼,以及最后的解決方法,推薦給小伙伴們2017-11-11
Android Canvas drawText文字居中的一些事(圖解)
這篇文章主要給大家介紹了關(guān)于Android Canvas drawText文字居中的一些事,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12
一文詳解Android無需權(quán)限調(diào)用系統(tǒng)相機拍照
這篇文章主要為大家介紹了Android無需權(quán)限調(diào)用系統(tǒng)相機拍照詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Android使用CardView作為RecyclerView的Item并實現(xiàn)拖拽和左滑刪除
這篇文章主要介紹了Android使用CardView作為RecyclerView的Item并實現(xiàn)拖拽和左滑刪除,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
Kotlin 與 Jetpack Compose 參數(shù)設(shè)計完全指南(最新推薦)
作為 Kotlin 和 Jetpack Compose 開發(fā)者,合理的參數(shù)設(shè)計能顯著提升代碼的可讀性和易用性,本文將系統(tǒng)整理各類參數(shù)規(guī)則,幫助您編寫更優(yōu)雅的 API,感興趣的朋友一起看看吧2025-04-04

