一步一步帶你實現自定義圓形進度條(詳解)
每次看到別人做出炫酷的都會想,這個應該很難吧?這是心理上先入為主的就這麼認為了,其實實現很簡單,下面一步一步的詳細剖析自定義圓形進度條的步驟。
首先看效果圖:
篇幅有點長,耐心看完肯定get新技能。
看每一個檢視都包含了些什麼。
- 最裡層一個藍色圓形
- 中間一層顯示進度的橙色扇形圓弧
- 最外層一個紅色圓環
- 顯示進度百分比的文字以及下方提示文字
下面來一步一步實現:
- 建立一個類繼承View,並實現幾個構造方法
- 定義樣式屬性,獲取屬性值
- 建立畫筆
- 重寫onDraw()繪製
- 應用
直接從第二步開始: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的值