1. 程式人生 > >一步一步帶你實現自定義圓形進度條(詳解)

一步一步帶你實現自定義圓形進度條(詳解)

        每次看到別人做出炫酷的都會想,這個應該很難吧?這是心理上先入為主的就這麼認為了,其實實現很簡單,下面一步一步的詳細剖析自定義圓形進度條的步驟。

首先看效果圖:

效果圖

篇幅有點長,耐心看完肯定get新技能。

看每一個檢視都包含了些什麼。

  • 最裡層一個藍色圓形
  • 中間一層顯示進度的橙色扇形圓弧
  • 最外層一個紅色圓環
  • 顯示進度百分比的文字以及下方提示文字

下面來一步一步實現:

  1. 建立一個類繼承View,並實現幾個構造方法
  2. 定義樣式屬性,獲取屬性值
  3. 建立畫筆
  4. 重寫onDraw()繪製
  5. 應用

直接從第二步開始:res->values下建立attrs.xml檔案

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleProgressView">
        // 裡層實心圓顏色
        <attr name="circleColor" format="color" />
        // 中間圓環(寬度/顏色)
        <attr name="progressWidth" format="dimension" />
        <attr name="progressColor"
format="color" />
// 外層圓環(寬度/顏色) <attr name="sectorWidth" format="dimension" /> <attr name="sectorColor" format="color" /> // 中間進度文字(顏色/大小) <attr name="proTextColor" format="color" /> <attr name="proTextSize" format="dimension" /> // 中間提示文字(顏色/大小/文字內容) <attr
name="tipTextColor" format="color" />
<attr name="tipTextSize" format="dimension" /> <attr name="tipText" format="string" /> // 最大進度 <attr name="max" format="integer" /> // 是否顯示最外層的圓環 <attr name="showStoke" format="boolean" /> // 進度圓環是否在圓上 <attr name="isAbove" format="boolean" /> // 進度是否滾動 <attr name="isScroll" format="boolean" /> </declare-styleable> </resources>
定義完屬性後該獲取定義的屬性了。 注:上方的文字大小和寬度必須用dimension[尺寸],而不能用float 宣告需要的變數
private Paint circlePaint; // 最裡層實心圓畫筆
    private int circleColor; // 實心圓顏色

    private Paint progressPaint; // 中間顯示進度圓環畫筆
    private float progressWidth; // 進度圓環寬度
    private int progressColor; // 進度圓環顏色

    private Paint sectorPaint; // 最外層圓環畫筆
    private float sectorWidth; // 外層圓環寬度
    private int sectorColor; // 外層圓環顏色

    private Paint proTextPaint;
    private float proTextSize;
    private int proTextColor;

    private Paint tipTextPaint;
    private float tipTextSize;
    private int tipTextColor;
    private String tipText;

    private int currProgress; // 當前進度
    private int maxProgress; // 最大進度

    private boolean isShow;
    private boolean isAbove;
    private boolean isScroll;

獲取屬性

private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleProgressView, 0, 0);
        circleColor = typedArray.getColor(R.styleable.CircleProgressView_circleColor, 0x993F51B5);
        progressWidth = typedArray.getDimension(R.styleable.CircleProgressView_progressWidth, 300);
        progressColor = typedArray.getColor(R.styleable.CircleProgressView_progressColor, 0x3F51B5);
        sectorWidth = typedArray.getDimension(R.styleable.CircleProgressView_sectorWidth, 10);
        sectorColor = typedArray.getColor(R.styleable.CircleProgressView_sectorColor, 0xFF4081);
        proTextSize = typedArray.getDimension(R.styleable.CircleProgressView_proTextSize, 60);
        proTextColor = typedArray.getColor(R.styleable.CircleProgressView_proTextColor, 0xFFFFFF);
        tipTextSize = typedArray.getDimension(R.styleable.CircleProgressView_tipTextSize, 30);
        tipTextColor = typedArray.getColor(R.styleable.CircleProgressView_tipTextColor, 0xFFFFFF);
        tipText = typedArray.getString(R.styleable.CircleProgressView_tipText);
        maxProgress = typedArray.getInteger(R.styleable.CircleProgressView_max, 100);
        isShow = typedArray.getBoolean(R.styleable.CircleProgressView_showStoke, true);
        isAbove = typedArray.getBoolean(R.styleable.CircleProgressView_isAbove, false);
        isScroll = typedArray.getBoolean(R.styleable.CircleProgressView_isScroll, false);
        typedArray.recycle();
    }

建立5個畫筆(1個圓,2個圓環,2個文字),然後在引數最多的構造器中呼叫

private void initPaint() {
        // 圓形畫筆
        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);
        circlePaint.setStyle(Paint.Style.FILL);
        // 進度圓環畫筆
        progressPaint = new Paint();
        progressPaint.setAntiAlias(true);
        progressPaint.setStyle(Paint.Style.STROKE);
        // 最外層圓環畫筆
        sectorPaint = new Paint();
        sectorPaint.setAntiAlias(true);
        sectorPaint.setStyle(Paint.Style.STROKE);
        // 進度文字畫筆
        proTextPaint = new Paint();
        proTextPaint.setAntiAlias(true);
        proTextPaint.setStyle(Paint.Style.FILL);
        // 提示文字畫筆
        tipTextPaint = new Paint();
        tipTextPaint.setAntiAlias(true);
        tipTextPaint.setStyle(Paint.Style.FILL);
    }

下面就是最重要的繪製過程了

        1、首先將獲取到的自定義屬性值設定給每個畫筆,在onDraw()方法中呼叫
        2、繪製最裡面的藍色圓形,確定圓心和半徑。假設在xml佈局中引用了這個View並設定width和height各位100dp,那麼圓心就應該在檢視的中心位置(50,50),半徑則是寬度或者高度的一半(50dp),知道了圓形和半徑就可以用canvas.drawCicle()繪製出一個圓。
如下圖:

圓心半徑
// getWidth為當前View的寬度,即上面設定的100dp
float circleRadius = getWidth() / 2;
// 引數:(圓心X座標,圓心Y座標,半徑,畫筆)
canvas.drawCircle(circleRadius, circleRadius, circleRadius, circlePaint);

這就繪製出了直徑為100dp的圓,為灰色矩形的內切圓(矩形加上背景作為對比),如圖:

圓形

        接著再在圓形外面畫一個緊貼著的圓環(圓心應該與藍色圓保持一致),假設圓環的寬度為sectorWidth = 5,想要圓環繪製在灰色矩形內,那麼藍色圓形的半徑就應該要縮小,那麼需要縮小多少呢?先縮小圓環的寬度來試試。

繪製圓環用的是

canvas.drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

需要傳入5個引數,oval是一個矩形,startAngle為圓環開始的角度,3點鐘的方向為0度,sweepAngle為掃過的角度(如360為一週),useCenter為false時繪製的為圓環,為true時繪製的是扇形,paint為畫筆。

第一個引數RectF又有幾個引數

RectF(float left, float top, float right, float bottom)

當此時我們要在灰色矩形中畫圓環,則將灰色矩形左上角的座標為(0,0),那麼這4個引數是用來確定圓環四個邊的位置的,如下圖:

這裡寫圖片描述

所以在灰色矩形中並且在藍色圓形外畫圓環就可以這樣畫,確定圓環的四個頂點位置並且將藍色圓的半徑縮小5,即:

RectF sector = new RectF(0, // left
                        0, // top
                        2 * circleRadius, // right
                        2 * circleRadius); // bottom
// 圓形
canvas.drawCircle(circleRadius, circleRadius, circleRadius - sectorWidth, circlePaint);
// 圓環
canvas.drawArc(sector, 0, 360, false, sectorPaint);
這裡寫圖片描述

從上圖可以看到,圓環是畫出來了,也是在藍色圓形外面,但是感覺好像哪裡不對。圓環的寬度有一半好像在矩形外面去了,而圓環與圓之間有空隙,圓環半徑應該再縮小圓環寬度/2就剛好填滿了空隙,由此我們可以知道 繪製圓環的半徑是(藍色圓的半徑+圓環寬度/2),當繪製的圓環有寬度時,圓環的外層要與矩形相切,因此藍色圓形的半徑還需要再縮小圓環寬度/2。

修改以上程式碼:

RectF sector = new RectF(sectorWidth/2, // left
                        sectorWidth/2, // top
                        2 * circleRadius - sectorWidth/2, // right
                        2 * circleRadius - sectorWidth/2); // bottom
// 圓形
canvas.drawCircle(circleRadius, circleRadius, circleRadius - sectorWidth/2, circlePaint);
// 圓環
canvas.drawArc(sector, 0, 360, false, sectorPaint);
這裡寫圖片描述

嗯,Perfect!

接下來在圓與圓環之間再畫出一個表示進度的圓弧。
思路很簡單,最外層的圓環不用動,將圓形半徑縮小圓弧寬度/2即可。繪製圓環和圓弧是一致的,只是掃過的角度不一致而已。

// 幾個頂點分別離X,Y軸的距離,progressWidth是進度圓弧的寬度
RectF progressRectF = new RectF(sectorWidth + progressWidth / 2,
                    sectorWidth + progressWidth / 2,
                    2 * circleRadius - sectorWidth - progressWidth / 2,
                    2 * circleRadius - sectorWidth - progressWidth / 2);
// -90度從圓的上頂點開始,掃描90度
canvas.drawArc(progressRectF, -90, 90, false, progressPaint);

如果動態的設定進度。
設: progress // 當前進度
        max // 最大進度百分比(100%則max = 100)
        swapAngel // 掃過的角度

則:swapAngel = (float)progress/max * 360;

canvas.drawArc(progressRectF, -90, swapAngel, false, progressPaint);
這裡寫圖片描述

Very Nice!!非常簡單


接下來就是繪製文字了,將顯示進度百分比的文字繪製在圓形的正中央。
繪製文字當然是用canvas.drawText()了,來看看它的幾個引數

drawText(String text, float x, float y, Paint paint)

第一個引數是要繪製的文字,第四個引數是畫筆,中間2個引數x,y不知道沒關係,我們先將x,y設定成圓心的座標試試看。

這裡寫圖片描述

我們得到了如圖左側的效果,但是想要的是右側的效果。對比可知,drawText()中的x,y引數分別指繪製文字左下角的橫縱座標。因此我們需要獲取到文字的寬高,
外層圓環半徑 - 文字寬度/2,外層圓環半徑 + 文字高度/2 就可以將文字移動到最中央。

// 獲取文字寬度,proText為繪製的進度文字
int width= proTextPaint.measureText(proText);
// 獲取文字高度
Rect rect = new Rect();
proTextPaint.getTextBounds(proText, 0, proText.length(), rect);
int height = rect.height();

獲取到寬高之後就可以用drawText繪製出進度文字了。

canvas.drawText(proText, circleRadius - width / 2,
                circleRadius + height / 2, proTextPaint);

繪製完進度,接下來該繼續繪製提示文字了。

這裡寫圖片描述

將“當前進度”放在下方圓半徑中間的位置,根據以上經驗可以輕鬆的寫出程式碼:

Rect tipRect = new Rect();
tipTextPaint.getTextBounds(tipText, 0, tipText.length(), tipRect);
int tipHeight = tipRect.height();
canvas.drawText(tipText, // 繪製的文字
        circleRadius - tipTextPaint.measureText(tipText) / 2,
        3 * circleRadius / 2 + tipHeight / 2, 
        tipTextPaint);

至於動態效果是在一個執行緒中用了一個臨時變數 temp 從0 ~ 設定的進度做迴圈逐漸增加,然後一次一次的繪製出來,不過感覺這樣很消耗效能,有更好的辦法歡迎聯絡我交流交流。具體程式碼就不展示了,歡迎下載Demo看看。

應用

XML :

在XML根元素中宣告名稱空間(hcc可換)

xmlns:hcc="http://schemas.android.com/apk/res-auto"
<com.cc.customview.progress.CircleProgressView
        android:id="@+id/pv"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true"
        hcc:isAbove="true"
        hcc:isScroll="true"
        hcc:proTextColor="#FFFFFF"
        hcc:proTextSize="30sp"
        hcc:progressColor="@color/colorOrange"
        hcc:progressWidth="5dp"
        hcc:sectorColor="@color/colorAccent"
        hcc:showStoke="true"
        hcc:tipText="當前進度"
        hcc:max="100" // 預設100,可填其他
        hcc:tipTextColor="#FFFFFF" />

Java :

pv.setCircleColor(getResources().getColor(R.color.colorPrimary));
pv.setAbove(false);
pv.setScroll(true);
pv.setShow(true);
pv.setProgressColor(getResources().getColor(R.color.colorOrange));
pv.setProTextColor(getResources().getColor(R.color.colorWhite));
pv.setTipTextColor(getResources().getColor(R.color.colorPrimary));
pv.setSectorColor(getResources().getColor(R.color.colorAccent));
pv.setTipText("當前進度");
pv.setTipTextColor(getResources().getColor(R.color.colorWhite));
pv.setProgressWidth(8);
pv.setSectorWidth(5);
pv.setMaxProgress(100); // 預設100,可填其他
CircleProgressView pv= (CircleProgressView) findViewById(R.id.pv);
pv.setProgress(80); // 任意整形大於0的值