背景
Android花樣loading進度條系列文章主要講解如何自定義所需的進度條,包括水準、圓形、環形、圓弧形、不規則形狀等。
本篇我們從圓形進度條講起,講簡單形式的環形進度條,隻有進度色彩,沒有進度文字,主要是使用Canvas繪制圓和圓弧。
效果
先上圖看效果,這裡有6個進度條,樣式上有微妙差別,基本都屬于一個類别的進度條了。

6個進度條基本上分為3類:
- 背景條與進度條同寬度;
- 背景條與進度條不同寬度的,進度條略小于背景條,呈現出層次效果;
- 圓環式或圓餅式的效果差別。
我們以第1種作為基本的形式來講述,需要準備的知識點有:
- 自定義控件的坐标軸;
- Canvas圓環、圓弧繪制方法;
- 自定義屬性;
- Handler消息處理機制。
其中涉及到的每一個知識點都有不少内容,我們隻提涉及到本次使用的知識内容。
知識點
1、自定義控件的坐标軸
Android系統的坐标軸示意圖如下,坐标軸原點O點,可以了解為螢幕的左上角。(圖檔來自網絡)
在自定義控件View的繪制過程中,O點則相當于自定義控件View區域的左上角,可以了解為View裡面有一個坐标系,坐标系的原點O在本View所在位置的左上角。
2、Canvas圓環、圓弧繪制方法
1)Canvas圓環繪制方法
android.graphics.Canvas#drawCircle(float cx, float cy, float radius, Paint paint)
此方法可繪制一個圓形,其中的參數:
- cx:圓心O的x軸坐标;
- cy:圓心O的y軸坐标;
- radius:圓的半徑;
- paint:繪制圖形所使用的畫筆。
如果畫筆使用空心模式,則繪制出一個圓環,如效果動畫中的圖1;如果畫筆使用實心模式,則繪制出一個圓餅,如效果動畫中的圖3。
2)Canvas圓弧繪制方法
android.graphics.Canvas#drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
此方法可以繪制一個圓弧,其中:
- oval:是一個矩形的區域,用以定義圓弧的外邊緣;
- sweepAngle:是圓弧掃過的角度;
- userCenter:用以控制圓弧在繪畫的時候,是否經過圓心;
- paint:繪制圖形所使用的畫筆。
3、自定義屬性
在自定義控件中我們免不了要用到各種顔色值、大小等參數。把這些值寫死在代碼裡使用者使用時就無法靈活控制,我們需要将屬性的值定義放到使用者端,以實作代碼的解耦。
此時就需要一些自定義屬性來支援我們對控件的屬性進行定義和使用。如圓環的寬度,我們可以定義為roundWidth,值使用dimension類型。
自定義屬性需要在項目res-values目錄下建立或修改attrs.xml檔案,具體使用方法可百度查閱,示例在下文給出。
4、Handler消息處理機制
Android中不允許子線程(我們寫個Thread運作時就算子線程)在主線程(可以了解為Activity)中更新UI界面的,比如設定text值、更改大小等,都是不行的。是以我們需要Handler中的消息機制來實作線上程中更新界面。
因為進度條控件在使用的時候都是配合着進度變化不斷變更的,通常進度變化都在子線程中來做,當progress值變化時,通過給Handler發送一個消息,攜帶progress值,Handler在接收消息後處理時将progress更新到UI界面上即可。
Handler的技術原理和具體使用要點可以百度查閱,這裡不展開了。下文有使用示例。
自定義屬性
有了上一段的知識點講解後,我們可以着實來繪制自定義控件了。繪制之前,我們需要了解下本控件有哪些屬性是需要單獨定義下,以供使用者使用時靈活修改的。初步考慮有:
- 清單内容
- 圓環的顔色;
- 圓環的寬度;
- 圓環上的進度顔色;
- 圓環上的進度寬度等。
為此我們定義attrs.xml配置檔案的内容為:
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<!--簡單環形進度-->
<declare-styleable name="SimpleRoundProgress">
<!--圓環顔色-->
<attr name="srp_roundColor" format="color" />
<!--圓環的寬度-->
<attr name="srp_roundWidth" format="dimension" />
<!--圓環上的進度顔色-->
<attr name="srp_progressColor" format="color" />
<!--圓環上的進度寬度-->
<attr name="srp_progressWidth" format="dimension" />
<!--進度值的最大值,一般為100-->
<attr name="srp_max" format="integer" />
<!--開始角度,指定進度初始點的繪制位置-->
<attr name="srp_startAngle" format="integer" />
<!--樣式,空心還是實心-->
<attr name="srp_style">
<enum name="STROKE" value="0" />
<enum name="FILL" value="1" />
</attr>
</declare-styleable>
</resources>
有了屬性定義後,我們在繪制的時候結合這些屬性就可以按設定值繪制了。
進度條圖形繪制
Android自定義圖形都需繼承View類,是以我們定義的進度條SimpleRoundProgress繼承View後,形如:
/**
* 簡單環形進度條
*/
public class SimpleRoundProgress extends View {
...
}
1、讀取自定義屬性,供後續使用
在SimpleRoundProgress類中定義一些成員變量,用來裝載自定義屬性的值,一會通過屬性加載類來加載屬性值。成員變量的定義有:
public class SimpleRoundProgress extends View {
private Paint paint; // 畫筆對象的引用
private int roundColor; // 圓環的顔色
private float roundWidth; // 圓環的寬度
private int progressColor; // 圓環進度的顔色
private float progressWidth; // 圓環進度的寬度
private int max; // 最大進度
private int style; // 進度的風格,實心或者空心
private int startAngle; // 進度條起始角度
public static final int STROKE = ; // 樣式:空心
public static final int FILL = ; // 樣式:實心
private int progress; // 目前進度
...
}
在構造函數中讀取使用者自定義屬性被賦予的值。
public class SimpleRoundProgress extends View {
...
public SimpleRoundProgress(Context context) {
this(context, null);
}
public SimpleRoundProgress(Context context, AttributeSet attrs) {
this(context, attrs, );
}
public SimpleRoundProgress(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint = new Paint();
// 讀取自定義屬性的值
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleRoundProgress);
// 擷取自定義屬性和預設值
roundColor = mTypedArray.getColor(R.styleable.SimpleRoundProgress_srp_roundColor, Color.RED);
roundWidth = mTypedArray.getDimension(R.styleable.SimpleRoundProgress_srp_roundWidth, );
progressColor = mTypedArray.getColor(R.styleable.SimpleRoundProgress_srp_progressColor, Color.GREEN);
progressWidth = mTypedArray.getDimension(R.styleable.SimpleRoundProgress_srp_progressWidth, roundWidth);
max = mTypedArray.getInteger(R.styleable.SimpleRoundProgress_srp_max, );
style = mTypedArray.getInt(R.styleable.SimpleRoundProgress_srp_style, );
startAngle = mTypedArray.getInt(R.styleable.SimpleRoundProgress_srp_startAngle, );
mTypedArray.recycle();
}
...
}
2、繪制進度環
繪制進度環主要有兩步:繪制背景環、繪制前景進度環。代碼均寫在View的onDraw方法體内:
public class SimpleRoundProgress extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
}
...
}
1)繪制背景環
因為是圓環型進度條,圓在繪制時圓心位置在整個控件的正中心,是以背景環的繪制代碼:
int centerX = getWidth() / ; // 擷取圓心的x坐标
int radius = (int) (centerX - roundWidth / ); // 圓環的半徑
// step1 畫最外層的大圓環
paint.setStrokeWidth(roundWidth); // 設定圓環的寬度
paint.setColor(roundColor); // 設定圓環的顔色
paint.setAntiAlias(true); // 消除鋸齒
// 設定畫筆樣式
switch (style) {
case STROKE:
paint.setStyle(Paint.Style.STROKE);
break;
case FILL:
paint.setStyle(Paint.Style.FILL_AND_STROKE);
break;
}
canvas.drawCircle(centerX, centerX, radius, paint); // 畫出圓環
作兩點說明:
- 圓心的x/y軸坐标值都是getWidth() / 2(一般寬高一樣);
- paint.setStyle可以設定畫筆是空心型還是實心型,對應的效果就是圓環或圓餅。
2)繪制前景進度環
前景進度環中有涉及到進度的計算,以總進度值為100為例,當進度在50時,如果是橫向進度條應該繪制整個控件一半的進度長度,如果是圓環型進度條,應該繪制180度的圓環。依次類推,進度環的角度計算為:
前景進度環的繪制代碼為:
// step2 畫圓弧-畫圓環的進度
paint.setStrokeWidth(progressWidth); // 設定畫筆的寬度使用進度條的寬度
paint.setColor(progressColor); // 設定進度的顔色
RectF oval = new RectF(centerX - radius , centerX - radius , centerX + radius , centerX + radius ); // 用于定義的圓弧的形狀和大小的界限
int sweepAngle = * progress / max; // 計算進度值在圓環所占的角度
// 根據進度畫圓弧
switch (style) {
case STROKE:
// 空心
canvas.drawArc(oval, startAngle, sweepAngle, false, paint);
break;
case FILL:
// 實心
canvas.drawArc(oval, startAngle, sweepAngle, true, paint);
break;
}
至此,進度條界面繪制就完成了,整個SimpleRoundProgress的代碼為:
package com.dommy.loading.widget;
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.util.AttributeSet;
import android.view.View;
import com.dommy.loading.R;
/**
* 簡單環形進度條
*/
public class SimpleRoundProgress extends View {
private Paint paint; // 畫筆對象的引用
private int roundColor; // 圓環的顔色
private float roundWidth; // 圓環的寬度
private int progressColor; // 圓環進度的顔色
private float progressWidth; // 圓環進度的寬度
private int max; // 最大進度
private int style; // 進度的風格,實心或者空心
private int startAngle; // 進度條起始角度
public static final int STROKE = ; // 樣式:空心
public static final int FILL = ; // 樣式:實心
private int progress; // 目前進度
public SimpleRoundProgress(Context context) {
this(context, null);
}
public SimpleRoundProgress(Context context, AttributeSet attrs) {
this(context, attrs, );
}
public SimpleRoundProgress(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint = new Paint();
// 讀取自定義屬性的值
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleRoundProgress);
// 擷取自定義屬性和預設值
roundColor = mTypedArray.getColor(R.styleable.SimpleRoundProgress_srp_roundColor, Color.RED);
roundWidth = mTypedArray.getDimension(R.styleable.SimpleRoundProgress_srp_roundWidth, );
progressColor = mTypedArray.getColor(R.styleable.SimpleRoundProgress_srp_progressColor, Color.GREEN);
progressWidth = mTypedArray.getDimension(R.styleable.SimpleRoundProgress_srp_progressWidth, roundWidth);
max = mTypedArray.getInteger(R.styleable.SimpleRoundProgress_srp_max, );
style = mTypedArray.getInt(R.styleable.SimpleRoundProgress_srp_style, );
startAngle = mTypedArray.getInt(R.styleable.SimpleRoundProgress_srp_startAngle, );
mTypedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getWidth() / ; // 擷取圓心的x坐标
int radius = (int) (centerX - roundWidth / ); // 圓環的半徑
// step1 畫最外層的大圓環
paint.setStrokeWidth(roundWidth); // 設定圓環的寬度
paint.setColor(roundColor); // 設定圓環的顔色
paint.setAntiAlias(true); // 消除鋸齒
// 設定畫筆樣式
switch (style) {
case STROKE:
paint.setStyle(Paint.Style.STROKE);
break;
case FILL:
paint.setStyle(Paint.Style.FILL_AND_STROKE);
break;
}
canvas.drawCircle(centerX, centerX, radius, paint); // 畫出圓環
// step2 畫圓弧-畫圓環的進度
paint.setStrokeWidth(progressWidth); // 設定畫筆的寬度使用進度條的寬度
paint.setColor(progressColor); // 設定進度的顔色
RectF oval = new RectF(centerX - radius , centerX - radius , centerX + radius , centerX + radius ); // 用于定義的圓弧的形狀和大小的界限
int sweepAngle = * progress / max; // 計算進度值在圓環所占的角度
// 根據進度畫圓弧
switch (style) {
case STROKE:
// 空心
canvas.drawArc(oval, startAngle, sweepAngle, false, paint);
break;
case FILL:
// 實心
canvas.drawArc(oval, startAngle, sweepAngle, true, paint);
break;
}
}
/**
* 設定進度的最大值
* <p>根據需要,最大值一般設定為100,也可以設定為1000、10000等</p>
*
* @param max int最大值
*/
public synchronized void setMax(int max) {
if (max < ) {
throw new IllegalArgumentException("max not less than 0");
}
this.max = max;
}
/**
* 擷取進度
*
* @return int 目前進度值
*/
public synchronized int getProgress() {
return progress;
}
/**
* 設定進度,此為線程安全控件
*
* @param progress 進度值
*/
public synchronized void setProgress(int progress) {
if (progress < ) {
throw new IllegalArgumentException("progress not less than 0");
}
if (progress > max) {
progress = max;
}
this.progress = progress;
// 重新整理界面調用postInvalidate()能在非UI線程重新整理
postInvalidate();
}
}
環形進度條的使用
1、頁面控件配置
自定義控件在頁面中使用時,要使用類名來作為标簽,自定義屬性要通過命名空間引入,整體如:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
...
tools:context="com.dommy.loading.SimpleRoundActivity">
...
<com.dommy.loading.widget.SimpleRoundProgress
android:id="@+id/srp_stroke_0"
android:layout_width="100dip"
android:layout_height="100dip"
android:layout_gravity="center"
app:srp_max="100"
app:srp_progressColor="@color/red_web"
app:srp_roundColor="@color/pro_bg"
app:srp_roundWidth="6dip"
app:srp_startAngle="0"
app:srp_style="STROKE" />
...
</LinearLayout>
界面編寫時的preview效果:
因為預設的progress為0,是以沒有前景進度顔色。
2、Activity代碼控制
通過方法com.dommy.loading.widget.SimpleRoundProgress#setProgress就可以設定控件顯示指定的進度,如
的效果為:
3、讓進度條動起來
當進度值為39時,如果要讓進度條動起來,可以讓進度值由0-39逐漸遞增顯示,通過一個線程控制進度值由0-39逐漸遞增的代碼:
private void refresh() {
final int percent = RandomUtil.getRandomPercent();
new Thread(new Runnable() {
Message msg = null;
@Override
public void run() {
int start = ;
while (start <= percent) {
msg = new Message();
msg.what = MSG_REFRESH_PROGRESS;
msg.arg1 = start;
handler.sendMessage(msg);
start++;
try {
Thread.sleep();
} catch (InterruptedException e) {
}
}
}
}).start();
}
這裡的percent是一個0-100的随機數,當percent為39時就是我們上述的顯示效果。
當線程更新start變量值後,通知handler更新界面狀态,handler的定義為:
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_REFRESH_PROGRESS:
srpStroke0.setProgress(msg.arg1);
break;
}
}
};
到這裡就實作了這樣的效果:
改變效果
由于在圖形繪制時讀取了自定義屬性的值,如果需要更改環形進度的樣式,直接通過屬性值就可以修改了,比較友善。文章最初貼出來的效果動畫中的6個環形進度,就是通過樣式更改的出來的,其實質上都是SimpleRoundProgress控件。
如果覺得控件的自定義屬性不夠用,可以自行添加和使用,在此基礎上進行擴充,做出你想要的進度效果即可。
源碼下載下傳
https://github.com/ahuyangdong/ColorfulLoading