1.前言
前天的瀏覽 GitHub 時發現一個模仿 Gif 的 Loading 特效的專案,感覺效果很不錯,也比較有創意,如下:
GitHub 上好幾個做這個效果的專案,但是很少有完全實現的,有的還有 Bug,於是花了 2 天實現了一下。
效果如下:
GitHub 專案在這裡 LeavesLoading
2. 分析
實現要求:
- 葉子
- 隨機產生
- 飄動軌跡為正弦函式,並且隨機振幅
- 飄動時伴隨自旋轉,更符合物理規律
- 遇到進度條似乎是融入的
- 風扇
- 可旋轉
- Loading == 100% 時顯示一個動畫
- 細節
- 風扇和葉子自適應 View 大小
- 葉子在視覺上不能飄出 RountRect 邊界
3. 核心實現
3.1 隨機產生葉子
本質是事先產生一定數量葉子,這些葉子的漂動時的振幅、相位、旋轉方向等等都是隨機的,並且飄動是週期性地即葉子飄動到最左邊時,又重新回到最右邊。
Leaf 類:
private class Leaf{
float x,y;//座標
AmplitudeType type;//葉子飄動振幅
int rotateAngle;//旋轉角度
RotateDir rotateDir;//旋轉方向
long startTime;//起始時間
int n;//初始相位
}
複製程式碼
Leaf 生成方法:
Leaf generateLeaf(){
Leaf leaf = new Leaf();
//隨機振幅
int randomType = mRandom.nextInt(3);
switch (randomType){
case 0:
//小振幅
leaf.type = AmplitudeType.LITTLE;
break;
case 1:
//中等振幅
leaf.type = AmplitudeType.MIDDLE;
break;
default:
//大振幅
leaf.type = AmplitudeType.BIG;
break;
}
//隨機旋轉方向
int dir = mRandom.nextInt(2);
switch (dir){
case 0:
//逆時針
leaf.rotateDir = RotateDir.ANTICLOCKWISE;
break;
default:
//順時針
leaf.rotateDir = RotateDir.CLOCKWISE;
break;
}
//隨機起始角度
leaf.rotateAngle = mRandom.nextInt(360);
leaf.n = mRandom.nextInt(20);
mAddTime += mRandom.nextInt((int)mLeafFloatTime);
leaf.startTime = System.currentTimeMillis() + mAddTime;
return leaf;
}
複製程式碼
3.2 葉子飄動軌跡為正弦函式
確定 Leaf 在某個時刻的座標 ( x , y ):
/**
* 獲取葉子的(x,y)位置
* @param leaf 葉子
* @param currentTime 當前時間
*/
private void getLeafLocation(Leaf leaf,long currentTime){
long intervalTime = currentTime - leaf.startTime;//飄動時長
if (intervalTime <= 0){
// 此 Leaf 還沒到飄動時間
return;
}else if (intervalTime > mLeafFloatTime){
// Leaf 的飄動時間大於指定的飄動時間,即葉子飄動到了最左邊,應回到最右邊
leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime);
}
// 計算移動因子
float fraction = (float) intervalTime / mLeafFloatTime;
leaf.x = (1-fraction)*mProgressLen;
leaf.y = getLeafLocationY(leaf);
if (leaf.x <= mYellowOvalHeight / 4){
//葉子飄到最左邊,有可能會超出 RoundRect 邊界,所以提前特殊處理
leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime);
leaf.x = mProgressLen;
leaf.y = getLeafLocationY(leaf);
}
}
複製程式碼
要想讓 Leaf 飄動軌跡為正弦函式,關鍵在於確定 Leaf 的 Y 軸座標:
/**
* 獲取葉子的Y軸座標
* @param leaf 葉子
* @return 經過計算的葉子Y軸座標
*/
private float getLeafLocationY(Leaf leaf){
float w = (float) (Math.PI * 2 / mProgressLen);//角頻率
float A;//計算振幅值
switch (leaf.type){
case LITTLE:
A = mLeafLen/3;
break;
case MIDDLE:
A = mLeafLen*2/3;
break;
default:
A = mLeafLen;
break;
}
// (mHeight-mLeafLen)/2 是為了讓 Leaf 的Y軸起始位置居中
return (float) (A * Math.sin(w * leaf.x + leaf.n)+(mHeight-mLeafLen)/2);
}
複製程式碼
3.3 葉子飄動時自旋轉
這裡就涉及到了 Leaf 的繪製,其實 Gif 中的葉子和風扇都可以使用 Canves 直接繪製圖案,但是這樣就會有兩個問題:
- 難畫:想要畫出滿意圖形,並且還要旋轉、縮放、平移可要下一番功夫。
- 靈活性低:如果想換其他樣式又得重新設計繪製過程。
因此這裡採用 Canves.drawBitmap()
的方式繪製,直接使用已有的圖片作為葉子和風扇,同時利用 Canves.drawBitmap()
的一個過載的方法可以很方便的實現旋轉、縮放、平移:
void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) ;
複製程式碼
就是通過這裡的 Matrix 矩陣,它內部封裝了 postScale()
、postTranslate
、postRotate()
等方法,可以幫助我們快速的對 Bitmap 進行旋轉、縮放、平移還有其他操作。使用時要記得配合 Canves 的 save()
和 restore()
使用,否則達不到想要的效果。
對這方面不熟的朋友可以看看 HenCoder 的自定義 View 教學 1-4 。
繪製 Leaf 的方法:
private void drawLeaves(Canvas canvas){
long currentTime = System.currentTimeMillis();
for (Leaf leaf : mLeafList) {
if (currentTime > leaf.startTime && leaf.startTime != 0){
// 獲取 leaf 當前的座標
getLeafLocation(leaf,currentTime);
canvas.save();
Matrix matrix = new Matrix();
// 縮放 自適應 View 的大小
float scaleX = (float) mLeafLen / mLeafBitmapWidth;
float scaleY = (float) mLeafLen / mLeafBitmapHeight;
matrix.postScale(scaleX,scaleY);
// 位移
float transX = leaf.x;
float transY = leaf.y;
matrix.postTranslate(transX,transY);
// 旋轉
// 計算旋轉因子
float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime)
/(float)mLeafRotateTime;
float rotate;
switch (leaf.rotateDir){
case CLOCKWISE:
//順時針
rotate = rotateFraction * 360 + leaf.rotateAngle;
break;
default:
//逆時針
rotate = -rotateFraction * 360 + leaf.rotateAngle;
break;
}
// 旋轉中心選擇 Leaf 的中心座標
matrix.postRotate(rotate,transX + mLeafLen / 2,transY + mLeafLen / 2);
canvas.drawBitmap(mLeafBitmap,matrix,mBitmapPaint);
canvas.restore();
}
}
複製程式碼
3.4 Loading == 100% 出現動畫
增加一個判斷欄位 isLoadingCompleted ,在 onDraw()
中選擇對應繪製策略。
isLoadingCompleted 在 setProgress()
中根據 progress 設定:
/**
* 設定進度(自動重新整理)
* @param progress 0-100
*/
public void setProgress(int progress){
if (progress < 0){
mProgress = 0;
}else if (progress > 100){
mProgress = 100;
}else {
mProgress = progress;
}
if (progress == 100){
isLoadingCompleted = true;
}else {
isLoadingCompleted = false;
}
// 255 不透明
mCompletedFanPaint.setAlpha(255);
postInvalidate();
}
複製程式碼
LeavesLoading.onDraw()
部分實現:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
if (isLoadingCompleted){
//繪製載入完成特效
drawCompleted(canvas);
}else {
//繪製扇葉
drawFan(canvas,mFanLen,mBitmapPaint);
}
//重新整理
postInvalidate();
}
複製程式碼
drawCompleted()
實現:
private void drawCompleted(Canvas canvas) {
// 每次繪製風扇透明度遞減10
int alpha = mCompletedFanPaint.getAlpha() - 10;
if (alpha <= 0){
alpha = 0;
}
mCompletedFanPaint.setAlpha(alpha);
// 文字透明度剛好與風扇相反
mCompletedTextPaint.setAlpha(255-alpha);
// 計算透明因子
float fraction = alpha / 255f;
// 葉片大小 和 文字大小 也是相反變化的
float fanLen = fraction * mFanLen;
float textSize = (1 - fraction) * mCompletedTextSize;
mCompletedTextPaint.setTextSize(textSize);
//測量文字佔用空間
Rect bounds = new Rect();
mCompletedTextPaint.getTextBounds(
LOADING_COMPLETED,
0,
LOADING_COMPLETED.length(),
bounds);
// 與 drawLeaf() 相似,不再贅述
drawFan(canvas, (int) fanLen, mCompletedFanPaint);
//畫文字
canvas.drawText(
LOADING_COMPLETED,
0,
LOADING_COMPLETED.length(),
mFanCx-bounds.width()/2f,
mFanCy+bounds.height()/2f,
mCompletedTextPaint);
}
複製程式碼
流程:計算風扇和文字透明度 -> 計算風扇和文字大小以及文字佔用空間 -> 繪製 ,註釋寫得比較清楚就不贅述了。
4. 結束
文章中如有出現任何錯誤,歡迎大家到評論區留言指正。
如果覺得 LeavesLoading 對您有任何幫助,希望可以在 GitHub 得到您的 Star !
Thanks: