Android 自定義圓形旋轉進度條,仿微博頭像載入效果

亦楓發表於2019-02-28

微博 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 集錦庫裡,地址如下。:

github.com/Mike-bel/an…

關於我:亦楓,部落格地址:yifeng.studio/,新浪微博:IT亦楓

微信掃描二維碼,歡迎關注我的個人公眾號:安卓筆記俠

不僅分享我的原創技術文章,還有程式設計師的職場遐想

相關文章