微博 App 的使用者頭像有一個圓形旋轉進度條的載入效果,看上去效果非常不錯,如圖所示:
據說 Instagram 也採用了這種效果。最近抽空研究了一下,最後實現的效果是這樣:
基本上能模擬出個大概,程式碼量不大,來講講實現思路吧。
模擬一種動畫效果,首先需要仔細分析其運作過程,找到其中的物理規律,從而確定實現方案。像這種運動速度較快的動畫,一般不是很容易看清。可以先通過錄屏軟體,錄取動畫運作的過程,然後藉助一些輔助工具放慢放大,比如 PS,反覆重複播放幾遍,基本上就能看出動畫的運作規律了。
回到這裡的載入效果,拆分開來,可以理解為畫筆上的兩層繪製和時間上的兩段過程。時間上,很明顯可以看出分為前 360 度和後 360 度,主要在畫筆上:
1,單一完整的圓弧繪製。前 360 度,從 360 度的圓弧到 0 度圓弧的遞減過程;後 360 度,從 0 度圓弧到 360 度圓弧的遞增過程。
2,重複片段的圓弧繪製。前 360 度,從零開始,逐漸遞增,直到多段填滿圓周;後 360 度,反過來,逐漸遞減,直到數量為零。
其他的就是細節的處理,後面具體實現時再提及,我們先來看看如何實現這兩個核心流程的繪製。
由於這兩個流程使用的畫筆屬性相同,所以使用一個 Paint 物件即可,這段程式碼沒什麼好講的,就是一些初始化工作:
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mArcWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);複製程式碼
注意,與微博原圖效果不同的是,我的效果圖中使用到了漸變色圓環,這樣效果更好看一些。使用 setShader() 方法可以給畫筆設定漸變色,實現方式是:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
......
Shader shader = new LinearGradient(0f, 0f, mWidth, mHeight,
mStartColor, mEndColor, Shader.TileMode.CLAMP);
mPaint.setShader(shader);
}複製程式碼
Shader 子類有很多,這裡使用的是線性漸變類 LinearGradient,由於需要使用 View 寬高,所以放在了 onSizeChanged() 函式裡面。
核心計算和繪製工作都在 onDraw() 方法裡面,看下這裡的程式碼:
@Override
protected void onDraw(Canvas canvas) {
if (maxAngle <= 360) {
float angle = 0;
canvas.rotate(mRatio * maxAngle / 360, mWidth / 2, mHeight / 2);
canvas.drawArc(mRectF, maxAngle, 360 - maxAngle, false, mPaint);
while (angle <= maxAngle) {
float length = mArcRadian * angle / maxAngle;
canvas.drawArc(mRectF, 0, length, false, mPaint);
canvas.rotate(mArcSpacing, mWidth / 2, mHeight / 2);
angle += mArcSpacing;
}
} else {
float angle = 0;
canvas.rotate(mRatio + mRatio * (maxAngle - 360) / 360, mWidth / 2, mHeight / 2);
canvas.drawArc(mRectF, 0, maxAngle - 360, false, mPaint);
canvas.rotate(maxAngle - 360, mWidth / 2, mHeight / 2);
while (angle <= 720 - maxAngle) {
float length = mArcRadian * angle / (720 - maxAngle);
canvas.drawArc(mRectF, 0, length, false, mPaint);
canvas.rotate(mArcSpacing, mWidth / 2, mHeight / 2);
angle += mArcSpacing;
}
}
if (maxAngle <= 720) {
maxAngle += mArcSpacing;
postInvalidateDelayed(30);
}
}複製程式碼
前面提到,動畫在時間上分前後 360 度的兩段過程,所以這裡定義了一個 maxAngle 變數來定義時間的變化。可以看到,前後 360 度的繪製程式碼看上去差不多,但還是有很大區別的。需要理解的地方有:
1,多個小段圓弧可以利用畫布旋轉的方式輕鬆實現,也就是 canvas.rotate() 方法,上面程式碼中的 while 迴圈部分。這裡有個細節處理,就是每段圓弧的弧度有個遞增變化。
2,整個 View 給人的感覺有一種旋轉的效果,為了實現這個效果,在每次繪製前,都增加了旋轉的步驟,也就是這行程式碼:
canvas.rotate(mRatio * maxAngle / 360, mWidth / 2, mHeight / 2);複製程式碼
其中 mRatio 表示整個動畫結束時,View 相比初始狀態時整體旋轉的角度。 這裡我設定的預設值是 60,值越大,動畫執行時呈現出越快的旋轉效果。大家可以修改原始碼,自己嘗試一下。
3,還有一點就是,maxAngle 變數每次增量值的設定,一定要設定為相鄰兩段片段圓弧的間距弧度。這樣做的目的是,保證每次繪製,片段圓弧都能有一個完整的遞增或遞減。否則,動畫執行時,看上會發生視覺上的抖動效果。
基本上就是這樣,程式碼量雖然不多,但是各種細節的處理還是耗費了很多時間調整。本來想使用一些圖例展示一下每行程式碼的作用,但是太費時間了。如果感興趣的,完全可以自己下載一下原始碼,修改試試看。
經過簡單地封裝,提取出一部分屬性:
<declare-styleable name="CircleLoadingView">
<attr name="circleStartColor" format="color|reference"/>
<attr name="circleEndColor" format="color|reference"/>
<attr name="circleArcWidth" format="integer|reference"/>
<attr name="circleArcRadian" format="integer|reference"/>
<attr name="circleArcSpacing" format="integer|reference"/>
</declare-styleable>複製程式碼
注意:中間的頭像部分不是這裡自定義 View 的內容,使用時可以自由填充內容。比如,看下我這裡的使用方式,還是比較自由的:
<RelativeLayout
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true">
<com.yifeng.view.view.CircleLoadingView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:circleStartColor="#ffff00"
app:circleEndColor="#ff0000"
app:circleArcRadian="5"
app:circleArcWidth="10"
app:circleArcSpacing="10"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:src="@mipmap/ic_avatar_default"/>
</RelativeLayout>複製程式碼
原始碼還是老樣子,統一在 GitHub 上的自定義 View 集錦庫裡,地址如下。:
關於我:亦楓,部落格地址:yifeng.studio/,新浪微博:IT亦楓
微信掃描二維碼,歡迎關注我的個人公眾號:安卓筆記俠
不僅分享我的原創技術文章,還有程式設計師的職場遐想