自定義View之漸變圓環(huán)進度條

前言

這兩天做的頁面中有一個比較有意思的積分環(huán)秽浇,采用的是漸變形式浮庐,具體如下圖所示。

ring.png

真實效果圖如下所示柬焕,

true_ring.png

下面就讓我們來一步步實現(xiàn)它审残。

對自定義View還不了解的可以參考我之前寫的三篇文章:

自定義View(一)(Android群英傳)

自定義View(二)(Android群英傳)

自定義View(三)(Android群英傳)

實現(xiàn)

首先梭域,我們要明確我們要控制這個自定義View的哪些屬性,可以分析出搅轿,我們需要控制環(huán)的粗細病涨,環(huán)的進度,進度環(huán)的起始和結束色璧坟,背景環(huán)的起始和結束色既穆,環(huán)的起始角度,環(huán)掃過的角度雀鹃,有了這些參數幻工,那么也就確定了其樣式,順便黎茎,我們再加一個是否顯示動畫的參數会钝,具體attrs.xml文件內容如下所示。

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="ProgressRing">
       <!--進度起始色-->
       <attr name="pr_progress_start_color" format="color" />
       <!--進度結束色-->
       <attr name="pr_progress_end_color" format="color" />
       <!--背景起始色-->
       <attr name="pr_bg_start_color" format="color" />
       <!--背景中間色-->
       <attr name="pr_bg_mid_color" format="color" />
       <!--背景結束色-->
       <attr name="pr_bg_end_color" format="color" />
       <!--進度值 介于0-100-->
       <attr name="pr_progress" format="integer" />
       <!--進度寬度-->
       <attr name="pr_progress_width" format="dimension" />
       <!--起始角度-->
       <attr name="pr_start_angle" format="integer" />
       <!--掃過的角度-->
       <attr name="pr_sweep_angle" format="integer" />
       <!--是否顯示動畫-->
       <attr name="pr_show_anim" format="boolean" />
   </declare-styleable>
</resources>

下一步新建一個類ProgressRing工三,繼承自View,我們實現(xiàn)其第二個構造函數

public ProgressRing(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

既然我們定義了那么多屬性先鱼,那么在這個構造函數中我們就需要用到那些屬性了俭正,具體操作如下所示。

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressRing);
progressStartColor = ta.getColor(R.styleable.ProgressRing_pr_progress_start_color, Color.YELLOW);
progressEndColor = ta.getColor(R.styleable.ProgressRing_pr_progress_end_color, progressStartColor);
bgStartColor = ta.getColor(R.styleable.ProgressRing_pr_bg_start_color, Color.LTGRAY);
bgMidColor = ta.getColor(R.styleable.ProgressRing_pr_bg_mid_color, bgStartColor);
bgEndColor = ta.getColor(R.styleable.ProgressRing_pr_bg_end_color, bgStartColor);
progress = ta.getInt(R.styleable.ProgressRing_pr_progress, 0);
progressWidth = ta.getDimension(R.styleable.ProgressRing_pr_progress_width, 8f);
startAngle = ta.getInt(R.styleable.ProgressRing_pr_start_angle, 150);
sweepAngle = ta.getInt(R.styleable.ProgressRing_pr_sweep_angle, 240);
showAnim = ta.getBoolean(R.styleable.ProgressRing_pr_show_anim, true);
ta.recycle();

這里startAngle為何為150呢焙畔,也就是起始角度為150度掸读,這150代表什么呢?因為我之后畫圓環(huán)調用的是drawArc函數宏多,我們跳轉進去查看一下便可發(fā)現(xiàn)如下說明The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 degrees (3 o'clock on a watch.)儿惫,也就是說,零度角是在時鐘三點鐘方向伸但,沿著順時針方向角度依次增大肾请,如果還不懂的話,那就看我下面這張圖的分析更胖。

zero_angle.png

所以sweepAngle為240大家也都知道了吧铛铁。

當然這都是默認值,具體數值你們可以自己定義却妨,從而實現(xiàn)不同的圓弧饵逐。

我們進度最大為100,我們可以得到單位角度unitAngle = (float) (sweepAngle / 100.0);

我們分別設置兩支畫筆彪标,bgPaintprogressPaint倍权,一支畫背景,一支畫進度捞烟,并對他們設置樣式薄声,具體如下所示当船。

private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
private Paint progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

bgPaint.setStyle(Paint.Style.STROKE);
bgPaint.setStrokeCap(Paint.Cap.ROUND);
bgPaint.setStrokeWidth(progressWidth);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeCap(Paint.Cap.ROUND);
progressPaint.setStrokeWidth(progressWidth);

現(xiàn)在,我們最初的準備工作都做完了奸柬,下面就是來確定畫的大小和畫的東西了生年,畫的位置,我們需要在onMeasure中確定畫的大小廓奕,代碼比較簡單抱婉,如下所示。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mMeasureWidth = getMeasuredWidth();
    mMeasureHeight = getMeasuredHeight();
    if (pRectF == null) {
        float halfProgressWidth = progressWidth / 2;
        pRectF = new RectF(halfProgressWidth + getPaddingLeft(),
                halfProgressWidth + getPaddingTop(),
                mMeasureWidth - halfProgressWidth - getPaddingRight(),
                mMeasureHeight - halfProgressWidth - getPaddingBottom());
    }
}

接下來就是最重要的怎么畫了桌粉,當然是在我們的onDraw函數中了蒸绩。我們先畫不支持動畫顯示的,從簡單的開始铃肯,我們先畫進度部分患亿,由于涉及到漸變,我們需要在起始色和結束色中間獲取過渡色押逼,我們調用如下函數即可步藕。

public int getGradient(float fraction, int startColor, int endColor) {
    if (fraction > 1) fraction = 1;
    int alphaStart = Color.alpha(startColor);
    int redStart = Color.red(startColor);
    int blueStart = Color.blue(startColor);
    int greenStart = Color.green(startColor);
    int alphaEnd = Color.alpha(endColor);
    int redEnd = Color.red(endColor);
    int blueEnd = Color.blue(endColor);
    int greenEnd = Color.green(endColor);
    int alphaDifference = alphaEnd - alphaStart;
    int redDifference = redEnd - redStart;
    int blueDifference = blueEnd - blueStart;
    int greenDifference = greenEnd - greenStart;
    int alphaCurrent = (int) (alphaStart + fraction * alphaDifference);
    int redCurrent = (int) (redStart + fraction * redDifference);
    int blueCurrent = (int) (blueStart + fraction * blueDifference);
    int greenCurrent = (int) (greenStart + fraction * greenDifference);
    return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent);
}

這里可能有小伙伴要說安卓API中就有漸變,比如LinearGradient挑格、SweepGradient咙冗、RadialGrdient,不過其局限性比較大漂彤,所以我還是選擇如上的方式獲取漸變色雾消。

我們從起始角startAngle開始畫,畫到startAngle + progress * unitAngle即可挫望,每次增加1弧度即可立润,相關代碼如下所示。

for (int i = 0, end = (int) (progress * unitAngle); i <= end; i++) {
    progressPaint.setColor(getGradient(i / (float) end, progressStartColor, progressEndColor));
    canvas.drawArc(pRectF,
            startAngle + i,
            1,
            false,
            progressPaint);
}

下圖顯示了進度為70的進度樣子媳板。

progress70.png

畫完了進度后桑腮,我們把進度相關代碼注釋掉,現(xiàn)在我們只關心畫背景色蛉幸,背景色我們只需要畫能看到部分即可到旦,不需要畫進度后面的背景色,這樣還提升了繪制效率巨缘。由于我們背景色還包含一個中間色添忘,所以還需要區(qū)分下左面和右面。相關代碼如下所示若锁。

float halfSweep = sweepAngle / 2;
for (int i = sweepAngle, st = (int) (progress * unitAngle); i > st; --i) {
    if (i - halfSweep > 0) {
        bgPaint.setColor(getGradient((i - halfSweep) / halfSweep, bgMidColor, bgEndColor));
    } else {
        bgPaint.setColor(getGradient((halfSweep - i) / halfSweep, bgMidColor, bgStartColor));
    }
    canvas.drawArc(pRectF,
            startAngle + i,
            1,
            false,
            bgPaint);
}

下圖顯示了進度為30的背景色樣子搁骑。

progress30.png

好了,我們把進度相關代碼取消注釋,再運行一遍便可得到最終效果仲器。

下面我們來為其加上動畫煤率,所謂動畫,就是讓其從進度為0開始畫乏冀,一點點畫到progress即可蝶糯,我們設置一個變量curProgress來表示當前進度,當curProgress < progress時辆沦,curProgress自增昼捍,再調用postInvalidate()即可,具體完整代碼如下所示肢扯。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * <pre>
 *     author: Blankj
 *     blog  : http://blankj.com
 *     time  : 2017/07/10
 *     desc  :
 * </pre>
 */
public class ProgressRing extends View {

    private int progressStartColor;
    private int progressEndColor;
    private int bgStartColor;
    private int bgMidColor;
    private int bgEndColor;
    private int progress;
    private float progressWidth;
    private int startAngle;
    private int sweepAngle;
    private boolean showAnim;

    private int mMeasureHeight;
    private int mMeasureWidth;

    private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    private Paint progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

    private RectF pRectF;

    private float unitAngle;

    private int curProgress = 0;

    public ProgressRing(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressRing);
        progressStartColor = ta.getColor(R.styleable.ProgressRing_pr_progress_start_color, Color.YELLOW);
        progressEndColor = ta.getColor(R.styleable.ProgressRing_pr_progress_end_color, progressStartColor);
        bgStartColor = ta.getColor(R.styleable.ProgressRing_pr_bg_start_color, Color.LTGRAY);
        bgMidColor = ta.getColor(R.styleable.ProgressRing_pr_bg_mid_color, bgStartColor);
        bgEndColor = ta.getColor(R.styleable.ProgressRing_pr_bg_end_color, bgStartColor);
        progress = ta.getInt(R.styleable.ProgressRing_pr_progress, 0);
        progressWidth = ta.getDimension(R.styleable.ProgressRing_pr_progress_width, 8f);
        startAngle = ta.getInt(R.styleable.ProgressRing_pr_start_angle, 150);
        sweepAngle = ta.getInt(R.styleable.ProgressRing_pr_sweep_angle, 240);
        showAnim = ta.getBoolean(R.styleable.ProgressRing_pr_show_anim, true);
        ta.recycle();

        unitAngle = (float) (sweepAngle / 100.0);

        bgPaint.setStyle(Paint.Style.STROKE);
        bgPaint.setStrokeCap(Paint.Cap.ROUND);
        bgPaint.setStrokeWidth(progressWidth);

        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeCap(Paint.Cap.ROUND);
        progressPaint.setStrokeWidth(progressWidth);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mMeasureWidth = getMeasuredWidth();
        mMeasureHeight = getMeasuredHeight();
        if (pRectF == null) {
            float halfProgressWidth = progressWidth / 2;
            pRectF = new RectF(halfProgressWidth + getPaddingLeft(),
                    halfProgressWidth + getPaddingTop(),
                    mMeasureWidth - halfProgressWidth - getPaddingRight(),
                    mMeasureHeight - halfProgressWidth - getPaddingBottom());
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (!showAnim) {
            curProgress = progress;
        }

        drawBg(canvas);
        drawProgress(canvas);

        if (curProgress < progress) {
            curProgress++;
            postInvalidate();
        }

    }

    // 只需要畫進度之外的背景即可
    private void drawBg(Canvas canvas) {
        float halfSweep = sweepAngle / 2;
        for (int i = sweepAngle, st = (int) (curProgress * unitAngle); i > st; --i) {
            if (i - halfSweep > 0) {
                bgPaint.setColor(getGradient((i - halfSweep) / halfSweep, bgMidColor, bgEndColor));
            } else {
                bgPaint.setColor(getGradient((halfSweep - i) / halfSweep, bgMidColor, bgStartColor));
            }
            canvas.drawArc(pRectF,
                    startAngle + i,
                    1,
                    false,
                    bgPaint);
        }
    }

    private void drawProgress(Canvas canvas) {
        for (int i = 0, end = (int) (curProgress * unitAngle); i <= end; i++) {
            progressPaint.setColor(getGradient(i / (float) end, progressStartColor, progressEndColor));
            canvas.drawArc(pRectF,
                    startAngle + i,
                    1,
                    false,
                    progressPaint);
        }
    }

    public void setProgress(@IntRange(from = 0, to = 100) int progress) {
        this.progress = progress;
        invalidate();
    }

    public int getProgress() {
        return progress;
    }

    public int getGradient(float fraction, int startColor, int endColor) {
        if (fraction > 1) fraction = 1;
        int alphaStart = Color.alpha(startColor);
        int redStart = Color.red(startColor);
        int blueStart = Color.blue(startColor);
        int greenStart = Color.green(startColor);
        int alphaEnd = Color.alpha(endColor);
        int redEnd = Color.red(endColor);
        int blueEnd = Color.blue(endColor);
        int greenEnd = Color.green(endColor);
        int alphaDifference = alphaEnd - alphaStart;
        int redDifference = redEnd - redStart;
        int blueDifference = blueEnd - blueStart;
        int greenDifference = greenEnd - greenStart;
        int alphaCurrent = (int) (alphaStart + fraction * alphaDifference);
        int redCurrent = (int) (redStart + fraction * redDifference);
        int blueCurrent = (int) (blueStart + fraction * blueDifference);
        int greenCurrent = (int) (greenStart + fraction * greenDifference);
        return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent);
    }
}

最后我們在布局中如下引用

<com.blankj.progressring.ProgressRing
    android:layout_width="320dp"
    android:layout_height="320dp"
    app:pr_bg_end_color="#00ffffff"
    app:pr_bg_mid_color="#33ffffff"
    app:pr_bg_start_color="#00ffffff"
    app:pr_progress="80"
    app:pr_progress_end_color="#D9B262"
    app:pr_progress_start_color="#00ffffff"
    app:pr_progress_width="8dp" />

最終效果如下所示妒茬。

progress.gif

結語

這次自定義View的實戰(zhàn)講解還是很具體細致的,希望大家能學到些什么蔚晨,比如如何一步步分析問題乍钻,解決問題的。還有就是能把初始化放在onDraw之外的就都放onDraw外铭腕,在onDraw中只做onDraw相關的银择,還有一些小細節(jié)就自己去發(fā)現(xiàn)吧。代碼已上傳GitHub累舷,喜歡的記得star哦欢摄。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市笋粟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌析蝴,老刑警劉巖害捕,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異闷畸,居然都是意外死亡尝盼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門佑菩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盾沫,“玉大人,你說我怎么就攤上這事殿漠「熬” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵绞幌,是天一觀的道長蕾哟。 經常有香客問我,道長,這世上最難降的妖魔是什么谭确? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任帘营,我火速辦了婚禮,結果婚禮上逐哈,老公的妹妹穿的比我還像新娘芬迄。我一直安慰自己,他們只是感情好昂秃,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布禀梳。 她就那樣靜靜地躺著,像睡著了一般械蹋。 火紅的嫁衣襯著肌膚如雪出皇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天哗戈,我揣著相機與錄音郊艘,去河邊找鬼。 笑死唯咬,一個胖子當著我的面吹牛纱注,可吹牛的內容都是我干的。 我是一名探鬼主播胆胰,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼狞贱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜀涨?” 一聲冷哼從身側響起瞎嬉,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厚柳,沒想到半個月后氧枣,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡别垮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年便监,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碳想。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡烧董,死狀恐怖,靈堂內的尸體忽然破棺而出胧奔,到底是詐尸還是另有隱情逊移,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布龙填,位于F島的核電站螟左,受9級特大地震影響啡浊,放射性物質發(fā)生泄漏。R本人自食惡果不足惜胶背,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一巷嚣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钳吟,春花似錦廷粒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至暇番,卻和暖如春嗤放,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背壁酬。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工次酌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舆乔。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓岳服,卻偏偏與公主長得像,于是被迫代替她去往敵國和親希俩。 傳聞我的和親對象是個殘疾皇子吊宋,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,283評論 25 707
  • 每次聽到某大牛談論自定義View,頓時敬佩之心颜武,如滔滔江水連綿不絕璃搜,心想我什么時候能有如此境界,好了鳞上,心動不如行動...
    Code4Android閱讀 2,169評論 6 25
  • 國內自定義View的文章汗牛充棟这吻,但是,即便是你全部看完也未必掌握這一知識(實際上因块,我也看了很多,但是一旦涉及自定...
    SnowDragonYY閱讀 1,643評論 3 36
  • 你留下名字,我贈你一句詩拒名》岳ⅲ或俗或雅。 禁止轉載增显,更不可抄襲或更改
    如巳閱讀 1,649評論 139 43
  • 本人制造3部檢驗員史春波雁佳,在外協(xié)檢驗斯麥爾合同號為2167991710時脐帝,9套車體組件未看到廠家的貨,便報工...
    波eifjhc閱讀 250評論 0 0