最近比較忙,煩心的事情也不少。就迷上了一款游戲《守望先鋒》,差點就沒回來。
言歸正傳,前些日子看到一個很炫酷的loadingView,看到的時候感覺,這個感覺怎么說呢,用英語說就是amazing(太TM吊了)。
我也僅僅只是通過別人的博客,加上一點自己的理解寫的這篇博客,目的是想要和大家分享,順便記錄一下。感覺實現一個這么炫酷的動畫還是感覺挺有成就感動的(畢竟菜鳥一枚)。這里先放上原博主的鏈接,感謝這位大神。這邊博客把實現過程已經寫的很清晰的建議有些自定義view基礎的人,先去看這里先放上原博主的博客,然后自己實現以下,我這里會對整個view的實現過程詳細的講一下。
此次時間有點倉促,沒有對代碼進行優化,同時也有部分原作者的代碼。希望大家諒解,主要是給大家提供一個思路。
先看一下效果圖:
怎么樣,我沒說錯吧,第一眼看見就眼前一亮。下面我們就對整個過程進行詳細的講解。
拆分動畫
- 和葉子一樣顏色的進度條
- 右側旋轉的白色電風扇
- 漂浮的葉子(原博主說的很細致我這里直接引用原話)
1.葉子的隨機產生;
2.葉子隨著一條正余弦曲線移動;
3.葉子在移動的時候旋轉,旋轉方向隨機,正時針或逆時針;
4.葉子遇到進度條,似乎是融合進入;
5.葉子不能超出最左邊的弧角;
7.葉子飄出時的角度不是一致,走的曲線的振幅也有差別,否則太有規律性,缺乏美感; - 最后又一個結束動畫,風扇消失,然后“100%”出現
整個動畫就是這樣子的,難點就是繪制葉子要滿足以上的7點。
定義屬性
private static final int DEFAULT_BG_OUTER = 0xfffde399; // 外部邊框的背景顏色
private static final String DEFAULT_WHITE = "#fffefd";
private static final int DEFAULT_BG_INNER = 0xffffa800; //內部進度條的顏色
private static final String DEFAULT_BG_FAN = "#fcce5b"; // 風扇 扇葉的顏色
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 600;
//振幅的強度
private static final int LOW_AMPLITUDE = 0;
private static final int NORMAL_AMPLITUDE = 1;
private static final int HIGH_AMPLITUDE = 2;
private static final int DEFAULT_AMPLITUDE = 20;
// 葉子飄動一個周期所花的時間
private static final int LEAF_FLY_TIME = 2000;
private static final int LEAF_ROTATE_TIME = 2000;
private Resources mResources;
// 定義畫筆
private Paint innerPaint;
private Paint outerPaint;
private Paint fanPaint;
private Paint fanBgPaint;
private Paint textPaint;
// view的大小 和 “100%”的高度
private int mWidth;
private int mHeight;
private float textHeight;
//外部圓半徑 內部圓半徑 風扇背景的半徑
private float outerRadius;
private float innerRadius;
private float fanBgRadius;
//各種路徑
private RectF outerCircle;
private RectF outerRectangle;
private RectF innerCircle;
private RectF innerRectangle;
private RectF fanWhiteRect;
//電風扇 扇葉路徑
private Path mPath;
private Path nPath;
// 定義結束的屬性動畫
private ValueAnimator progressAnimator;
private ValueAnimator completedAnimator;
//進度值
private float maxProgress = 100;
private float currentProgress;
private float completedProgress;
//計算時間增量和progress增量
private long preTime ;
private long addTime;
private float addProgress;
private float preProgress;
//先填充半圓的進度 和 長方形的時間
private float firstStepTime;
private float secondStepTime;
//和葉片相關
private Bitmap mLeafBitmap;
private int mLeafWidth;
private int mLeafHeight;
private int mLeafFlyTime = LEAF_FLY_TIME;
private int mLeafRotateTime = LEAF_ROTATE_TIME;
private int mAddTime;
private float mAmplitudeDisparity = DEFAULT_AMPLITUDE;
//判斷是否加載完畢 然后執行結束動畫
private boolean isFinished;
//精度條的總長度
private float mProgressWidth;
private List<Leaf> leafInfos;
//對 外面的邊框緩存
private WeakReference<Bitmap> outBorderBitmapCache;
這里定義的屬性比較多,但是還是都通熟易懂的。
OnDraw()
我們這先看一下onDraw方法吧,整個的繪制流程是都放生在這個方法里面。我們先梳理一下繪制的流程,具體畫每個圖形后面我會詳細講解。
protected void onDraw(Canvas canvas) {
//判斷背景有沒有緩存(這里的背景是指,黃色進度條外面的邊框)
Bitmap outBorderBitmap = outBorderBitmapCache == null ? null : outBorderBitmapCache.get();
if (outBorderBitmap == null || outBorderBitmap.isRecycled()) {
outBorderBitmap = getBitmap();
outBorderBitmapCache = new WeakReference<Bitmap>(outBorderBitmap);
}
//對畫布保存主要是要用Xfermode對圖像處理,主要是不想讓葉子飛出邊界
//如果不了解Xfermode的同學建議先去看一下,很有用的一個東西
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null, Canvas.MATRIX_SAVE_FLAG |
Canvas.CLIP_SAVE_FLAG |
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
Canvas.CLIP_TO_LAYER_SAVE_FLAG);
canvas.drawBitmap(outBorderBitmap, 0, 0, outerPaint);
// canvas.translate(mWidth / 10, mHeight / 2);
//畫葉子
drawLeaf(canvas);
//恢復畫布
canvas.restoreToCount(sc);
canvas.translate(mWidth / 10, mHeight / 2);
//畫內部圓
drawInnerCircle(canvas);
//畫風扇白色的背景
canvas.drawArc(fanWhiteRect, 90, 360, true, fanPaint);
//畫風扇的黃色背景
canvas.save();
canvas.scale(0.9f, 0.9f, 8 * outerRadius, 0);
canvas.drawArc(fanWhiteRect, 90, 360, true, fanBgPaint);
canvas.restore();
//畫扇葉
canvas.save();
drawFan(canvas, true);
canvas.restore();
//結束動畫
//結束動畫是指 電風扇的扇葉從扇葉變成100%字樣
if (isFinished) {
showCompletedText(canvas);
} else {
//這里重新繪制 主要是為了畫葉子
invalidate();
}
}
首先我們先說一下畫背景(這里的背景指的是進度條外面的邊框)
先看一下具體實現
public Bitmap getBitmap() {
//這里先產生一個一個畫布,畫布的大小就是view的大小
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight,Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.translate(mWidth / 10, mHeight / 2);
canvas.drawArc(outerCircle, 90, 180, true, outerPaint);
canvas.drawRect(outerRectangle, outerPaint);
return bitmap;
}
這里我們讓背景作為bitmap返回主要是 要使用Xfermode方法。先移動坐標系到我們想要的位置,然后背景是由一個左半圓和一個矩形組成。至于為什么要使用Xfermode,這里先說明一下,我們期望葉子是不可以飄出背景外的。(就是說葉子飄出背景外的地方要變成透明的)。
畫進度條
//先填充半圓
private void drawInnerCircle(Canvas canvas) {
firstStepProgress = innerRadius / (innerRadius + 7 * outerRadius);
if (currentProgress > firstStepProgress) {
canvas.drawArc(innerCircle, 90, 180, true, innerPaint);
drawInnerRectangle(canvas);
} else {
//這里就是繪制半圓的執行(方法是繪制圓弧)
canvas.drawArc(innerCircle, 180 - 90 * currentProgress / firstStepTime, 180 * currentProgress / firstStepTime, false, innerPaint);
}
}
//填充剩下的長方形
private void drawInnerRectangle(Canvas canvas) {
secondStepProgress = 1 - firstStepProgress;
//判斷是否結束,結束了會執行結束動畫
if (currentProgress >= 1) {
if (!isFinished) {
isFinished = true;
completedAnimator.start();
}
} else {
canvas.drawRect(-1, -innerRadius, 7 * outerRadius * (currentProgress - firstStepProgress) / secondStepProgress, innerRadius, innerPaint);
}
}
進度條和背景是一樣的,都是先都前半圓和一個矩形組成的。先計算半圓所占進度,當currentProgress沒有超過firstStepProgress時候,先繪制半圓部分,之后繪制矩形。
繪制風扇
參數分別是 canvas 畫布,isNeedRotate 風扇是否旋轉。
//畫扇葉
private void drawFan(Canvas canvas, boolean isNeedRotate) {
canvas.save();
//加載的時候旋轉風扇,負數是逆時針旋轉,默認旋轉5圈
if (isNeedRotate) {
canvas.rotate(-currentProgress * 360 * 5, 8 * outerRadius, 0);
}
//結束動畫時候需要不斷的縮小風扇,然后“100%”從小變大
if (completedProgress != 0) {
canvas.scale(1 - completedProgress, 1 - completedProgress, 8 * outerRadius, 0);
}
//旋轉畫扇葉,扇葉使用path繪制的
for (float i = 0; i <= 270; i = i + 90) {
canvas.rotate(i, 8 * outerRadius, 0);
canvas.drawPath(mPath, fanPaint);
}
//這個是風扇中間的小點
canvas.drawCircle(8 * outerRadius, 0, 5 * (1 - completedProgress), fanPaint);
canvas.restore();
}
繪制結束動畫
結束動畫 這里我們用的是屬性動畫提供的0-1的值實現的。這個過程主要是把進度條補齊以及風扇消失,然后“100%”字樣顯示。
//結束時動畫 展示“100%”字樣
private void showCompletedText(Canvas canvas) {
//補齊進度條
canvas.drawRect(-1, -innerRadius, (7 + completedProgress) * outerRadius, innerRadius, innerPaint);
canvas.drawArc(fanWhiteRect, 90, 360, true, fanPaint);
//繪制風扇的背景
canvas.save();
canvas.scale(0.9f, 0.9f, 8 * outerRadius, 0);
canvas.drawArc(fanWhiteRect, 90, 360, true, fanBgPaint);
canvas.restore();
if (completedProgress == 1) {
textPaint.setTextSize(60);
canvas.drawText("100%", 8 * outerRadius, textHeight, textPaint);
} else {
drawFan(canvas, completedProgress, false);
textPaint.setTextSize(60 * completedProgress);
canvas.drawText("100%", 8 * outerRadius, textHeight, textPaint);
}
}
繪制葉子
因為葉子是一直在飄蕩的,這里利用系統的時間,來計算葉子的坐標。
private class Leaf {
// 在繪制部分的位置
float x, y;
// 控制葉子飄動的幅度
int type;
// 旋轉角度
int rotateAngle;
// 旋轉方向--0代表順時針,1代表逆時針
int rotateDirection;
// 起始時間(ms)
long startTime;
}
/**
* 畫葉子
*/
private void drawLeaf(Canvas canvas) {
long currentTime = System.currentTimeMillis();
canvas.save();
//這里進行了 一次畫布平移
canvas.translate(mWidth / 10 - innerRadius, mHeight / 2 - outerRadius);
for (Leaf leaf : leafInfos) {
//如果系統當前的時間大于葉子開始繪制的時間,就去獲取葉子的坐標
if (currentTime > leaf.startTime && leaf.startTime != 0) {
getLocation(leaf, currentTime);
// 通過時間關聯旋轉角度,則可以直接通過修改LEAF_ROTATE_TIME調節葉子旋轉快慢
float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime)
/ (float) mLeafRotateTime;
int angle = (int) (rotateFraction * 360);
int rotate = leaf.rotateDirection == 0 ? angle + leaf.rotateAngle : -angle
+ leaf.rotateAngle;
//用矩陣進行坐標轉換
Matrix matrix = new Matrix();
matrix.reset();
matrix.postTranslate(leaf.x, leaf.y);
matrix.postRotate(rotate, leaf.x + mLeafWidth / 2, leaf.y + mLeafHeight / 2);
//對畫筆設置Xfermode
outerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
canvas.drawBitmap(mLeafBitmap, matrix, outerPaint);
outerPaint.setXfermode(null);
} else {
continue;
}
}
canvas.restore();
}
//獲取葉子當前的位置
public void getLocation(Leaf leaf, long currentTime) {
//計算當前的時間和葉子繪制的時間的差值
long intervalTime = currentTime - leaf.startTime;
if (intervalTime < 0) {
//不對此片葉子進行繪制,還沒到它出場的時間
return;
} else if (intervalTime > mLeafFlyTime) {
//重置葉子的出場時間
leaf.startTime = System.currentTimeMillis()
+ new Random().nextInt(mLeafFlyTime);
}
float fraction = (float) intervalTime / mLeafFlyTime;
leaf.x = getLeafX(fraction);
leaf.y = getLeafY(leaf);
}
//獲取葉子x坐標
public float getLeafX(float fraction) {
return mProgressWidth * (1 - fraction);
}
//獲取葉子y坐標,用到sin函數,多處用到random是為了讓葉子顯的更加自然
public float getLeafY(Leaf leaf) {
float w = (float) (2 * Math.PI / mProgressWidth);
float a = outerRadius / 2;
switch (leaf.type) {
case LOW_AMPLITUDE:
// 小振幅 = 中等振幅 - 振幅差
a = -mAmplitudeDisparity;
break;
case NORMAL_AMPLITUDE:
break;
case HIGH_AMPLITUDE:
// 小振幅 = 中等振幅 + 振幅差
a = +mAmplitudeDisparity;
break;
default:
break;
}
return (float) (a * Math.sin((w * leaf.x))) - mLeafHeight / 2 + outerRadius;
}
最后放上效果圖
可能看著和原著有點.......,嘿嘿,原諒我沒有進行優化,大家看看思路就可以了。代碼地址
本文參考了一個絢麗的loading動效分析與實現!