最近作項目碰到一個這樣的一個需求:須要一個環形的進度條表示一個下載請求的進度加載。 同時要以各類不一樣的圖標展示其下載過程當中的各個狀態:等待、下載中、暫停、錯誤、完成。java
具體狀態對應圖標見下圖: android
以上圖標來自www.iconfont.cn/。git
考慮到其狀態多達 5 種之多。用已有的控件組合顯示,而後判斷狀態來控制各圖標的顯示不太合適。 藉此機會,簡單的擼一個這樣的一個自定義控件:CircleProgressBar 來溫習下自定義控件的知識。github
直接拷貝 CircleProgressBar 使用:CircleProgressBar.javacanvas
首先須要的基礎知識,你須要瞭解關於安卓自定義控件的基本原理、控件的繪製過程。 推薦看下官方的相關文檔 Custom View Components。注意:文檔爲英文文檔,有牆。markdown
簡單總結下見下表: app
搞清楚上面的基礎以後就正式開始自定義控件。若是尚未看過上述文檔也能夠跟着我把下面的步奏寫一遍。ide
通常自定義 View 都是繼承自 android.view.View。不過既然咱們自定義的是 ProgressBar,就不必重頭開始了,直接繼承自 android.widget.ProgressBar 。 這樣 setProgress(int progress); 這些基礎方法就不必再定義了。So,給個人控件取名爲 CircleProgressBar extends ProgressBar
。oop
觀察上述幾個圖標,除了下載中狀態有進度加載,其形態有所改變外,其他狀態均爲一個靜態圖片。如今只用搞定下載中狀態的圓環進度和繪製中間的兩條豎線便可。佈局
咱們在使用 Android SDK 提供的控件的時候,能夠直接從 .xml
文件中新建,好比新建一個 LinearLayout:
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" /> 複製代碼
同時咱們還能夠直接在 .xml
文件中配置各類屬性,如上述代碼中的 android:orientation="horizontal"
。 咱們自定義的控件固然也要支持配置和一些自定義屬性,因此就必需要這個構造方法:public CircleProgressBar(Context context, AttributeSet attrs) {}
。 這個構造方法容許咱們在 .xml
文件中建立和編輯咱們自定義控件的實例:
public CircleProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } 複製代碼
同時,爲了在 .xml
文件中定義咱們的自定義屬性(eg: color, size, etc.),咱們須要新增如下構造方法:
public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } 複製代碼
defStyleAttr 這個整型變量是一個定義在 res/values/attrs.xml
文件中的 declare-styleable
值。 基於此,咱們須要新建 res/values/attrs.xml
文件,並定義一些須要用到的自定義屬性。
觀察要實現的外圈進度條,有兩個進度:一個用來表示默認的圓形,另外一個表示進度的顏色。因此這裏涉及到兩個進度條顏色寬高的定義。要繪製圓確定還須要半徑。 故全部定義的屬性以下:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleProgressBar"> <!--默認圓的顏色--> <attr name="defaultColor" format="color" /> <!--進度條的顏色--> <attr name="reachedColor" format="color" /> <!--默認圓的高度--> <attr name="defaultHeight" format="dimension" /> <!--進度條的高度--> <attr name="reachedHeight" format="dimension" /> <!--圓的半徑--> <attr name="radius" format="dimension" /> </declare-styleable> </resources> 複製代碼
這段代碼聲明瞭 5 個自定義屬性,它們都是屬於 styleable:CircleProgressBar 的。 爲了方便起見,通常styleable的name和咱們自定義控件的類名同樣。自定義控件定義好了以後就能夠直接使用了。 具體自定義屬性值含義見 xml 裏面的註釋。
在使用中就能夠直接設置這些自定義屬性了:
<com.chengww.circleprogressdemo.CircleProgressBar android:layout_width="46dp" android:layout_height="46dp" android:padding="6dp" android:id="@+id/cp_progress" app:defaultColor="#D8D8D8" app:reachedColor="#1296DB" app:defaultHeight="2.5dp" app:reachedHeight="2.5dp" /> 複製代碼
既然定義了自定義屬性,固然須要獲取到具體使用中設置的自定義屬性。不然定義自定義屬性就沒有意義了。 首先定義成員變量:
private int mDefaultColor; private int mReachedColor; private int mDefaultHeight; private int mReachedHeight; private int mRadius; private Paint mPaint; private Status mStatus = Status.Waiting; 複製代碼
而後就是獲取成員變量了。還記得咱們上文中 Java 代碼裏面定義的構造方法 public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {}
嗎? 沒錯,就是在這個方法裏面獲取用戶設置的自定義屬性值:
public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); //默認圓的顏色 mDefaultColor = typedArray.getColor(R.styleable.CircleProgressBar_defaultColor, Color.parseColor("#D8D8D8")); //進度條的顏色 mReachedColor = typedArray.getColor(R.styleable.CircleProgressBar_reachedColor, Color.parseColor("#1296DB")); //默認圓的高度 mDefaultHeight = typedArray.getDimension(R.styleable.CircleProgressBar_defaultHeight, dp2px(context, 2.5f)); //進度條的高度 mReachedHeight = typedArray.getDimension(R.styleable.CircleProgressBar_reachedHeight, dp2px(context, 2.5f)); //圓的半徑 mRadius = typedArray.getDimension(R.styleable.CircleProgressBar_radius, dp2px(context, 17)); typedArray.recycle(); setPaint(); } 複製代碼
當咱們在 xml 文件中建立一個 View 時,全部在 xml 文件中聲明的屬性都會被傳入到該 View 的上述構造方法中。 經過調用 Context 的 obtainStyledAttributes() 方法返回一個 TypedArray 對象。而後直接用 TypedArray 對象獲取自定義屬性的值,第二個參數是獲取不到時取得默認值。 因爲 TypedArray 對象是共享的資源,因此在獲取完值以後必需要調用 recycle() 方法來回收。
上述方法只能經過 xml 文件設置自定義屬性,只有在 View 被初始化的時候才能獲取到。要想在運行時使用 Java 方法修改某個屬性值,對某個屬性值(成員變量)新增 Getter 和 Setter 方法便可。
private Status mStatus = Status.Waiting; public Status getStatus() { return mStatus; } public void setStatus(Status status) { if (mStatus == status) return; mStatus = status; invalidate(); } 複製代碼
注意 setStatus 方法,在爲 mStatus 賦值以後,調用了 invalidate() 方法,咱們自定義控件的屬性發生改變以後,控件的樣子也可能發生改變,在這種狀況下就須要調用 invalidate() 方法讓系統去調用 View 的 onDraw() 從新繪製。 一樣的,控件屬性的改變可能致使控件所佔的大小和形狀發生改變,能夠調用 requestLayout() 來請求測量獲取一個新的佈局位置。 注:如改變某屬性後,肯定控件不會變動大小和位置,能夠不須要調用 requestLayout() 方法。一樣,如控件不須要重繪,能夠不須要調用 invalidate() 方法。
獲取基礎的一些屬性,這裏 mStatus 用來表示當前 View 的狀態以對應各類下載狀態。咱們用這些狀態來斷定如何繪製合適的效果。各狀態用一個內部枚舉來表示。
public enum Status { Waiting, Pause, Loading, Error, Finish } 複製代碼
上述 setPaint() 爲初始化 paint 方法。用以繪製進度圓環和各靜態 Drawable。附上 setPaint() 方法代碼:
private void setPaint() { mPaint = new Paint(); //下面是設置畫筆的一些屬性 mPaint.setAntiAlias(true);//抗鋸齒 mPaint.setDither(true);//防抖動,繪製出來的圖要更加柔和清晰 mPaint.setStyle(Paint.Style.STROKE);//設置填充樣式 /** * Paint.Style.FILL :填充內部 * Paint.Style.FILL_AND_STROKE :填充內部和描邊 * Paint.Style.STROKE :僅描邊 */ mPaint.setStrokeCap(Paint.Cap.ROUND);//設置畫筆筆刷類型 } 複製代碼
一個 View 在展現時老是其寬和高,測量 View 就是爲了可以讓自定義的控件可以根據各類不一樣的狀況以合適的寬高去展現。 具體使用到的方法爲 onMeasure() 方法。該方法重寫自系統的方法,包含兩個參數:int widthMeasureSpec, int heightMeasureSpec。 這兩個參數包含了兩個重要的信息:Mode 和 Size。獲取 Mode 和 Size:
int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); 複製代碼
以上代碼能夠獲取 widthMode、heightMode、widthSize、heightSize 共四個參數。
Mode 表明了當前控件的父控件告訴咱們控件,你應該按怎樣的方式來佈局。 Mode 有三個可選值:EXACTLY、AT_MOST、UNSPECIFIED。它們的含義是:
Size 其實就是父佈局傳遞過來的一個大小,父佈局但願當前佈局的大小。
下面是咱們代碼中 onMeasure() 方法的寫法:
@Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int paintHeight = Math.max(mReachedHeight, mDefaultHeight); if (heightMode != MeasureSpec.EXACTLY) { int exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius * 2 + paintHeight; heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY); } if (widthMode != MeasureSpec.EXACTLY) { int exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius * 2 + paintHeight; widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } 複製代碼
咱們只須要處理寬高沒有精確指定的狀況,經過 padding 加上整個圓以及 Paint 的寬度計算出具體的值。
接下來就是繪製效果了。
如開始所述:觀察上述幾個圖標,除了下載中狀態有進度加載,其形態有所改變外,其他狀態均爲一個靜態圖片。繪製其他狀態靜態圖片可使用: drawable.draw(canvas);
方法。如今說說如何繪製下載中這個狀態。
重寫 onDraw() 方法,而後咱們開始繪製圓:
canvas.translate(getPaddingStart(), getPaddingTop()); mPaint.setStyle(Paint.Style.STROKE); //畫默認圓(邊框)的一些設置 mPaint.setColor(mDefaultColor); mPaint.setStrokeWidth(mDefaultHeight); canvas.drawCircle(mRadius, mRadius, mRadius, mPaint); 複製代碼
經過 canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
繪製默認狀態下的圓。以後改變畫筆的顏色,根據進度繪製圓弧。
//畫進度條的一些設置 mPaint.setColor(mReachedColor); mPaint.setStrokeWidth(mReachedHeight); //根據進度繪製圓弧 float sweepAngle = getProgress() * 1.0f / getMax() * 360; canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius * 2), -90, sweepAngle, false, mPaint); 複製代碼
最後繪製圓中間的兩條豎線下載中狀態就完成了。下面是一個示例,繪製豎線寬度爲 2/5 半徑(1/5 + 1/5),高度爲 1/2 半徑(1/2 + 1/2):
mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(dp2px(getContext(), 2)); mPaint.setColor(Color.parseColor("#667380")); canvas.drawLine(mRadius * 4 / 5, mRadius * 3 / 4, mRadius * 4 / 5, 2 * mRadius - (mRadius * 3 / 4), mPaint); canvas.drawLine(2 * mRadius - (mRadius * 4 / 5), mRadius * 3 / 4, 2 * mRadius - (mRadius * 4 / 5), 2 * mRadius - (mRadius * 3 / 4), mPaint); 複製代碼
而後經過判斷 mStatus 來繪製不一樣的狀態便可完成 onDraw() 方法便可。完整 onDraw() 代碼和相關 dp2px 方法:
@Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); /** * 這裏canvas.save();和canvas.restore();是兩個相互匹配出現的,做用是用來保存畫布的狀態和取出保存的狀態的 * 當咱們對畫布進行旋轉,縮放,平移等操做的時候其實咱們是想對特定的元素進行操做,可是當你用canvas的方法來進行這些操做的時候,實際上是對整個畫布進行了操做, * 那麼以後在畫布上的元素都會受到影響,因此咱們在操做以前調用canvas.save()來保存畫布當前的狀態,當操做以後取出以前保存過的狀態, * (好比:前面元素設置了平移或旋轉的操做後,下一個元素在進行繪製以前執行了canvas.save();和canvas.restore()操做)這樣後面的元素就不會受到(平移或旋轉的)影響 */ canvas.save(); //爲了保證最外層的圓弧所有顯示,咱們一般會設置自定義view的padding屬性,這樣就有了內邊距,因此畫筆應該平移到內邊距的位置,這樣畫筆纔會恰好在最外層的圓弧上 //畫筆平移到指定paddingLeft, getPaddingTop()位置 canvas.translate(getPaddingStart(), getPaddingTop()); int mDiameter = (int) (mRadius * 2); if (mStatus == Status.Loading) { mPaint.setStyle(Paint.Style.STROKE); //畫默認圓(邊框)的一些設置 mPaint.setColor(mDefaultColor); mPaint.setStrokeWidth(mDefaultHeight); canvas.drawCircle(mRadius, mRadius, mRadius, mPaint); //畫進度條的一些設置 mPaint.setColor(mReachedColor); mPaint.setStrokeWidth(mReachedHeight); //根據進度繪製圓弧 float sweepAngle = getProgress() * 1.0f / getMax() * 360; canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius * 2), -90, sweepAngle, false, mPaint); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(dp2px(getContext(), 2)); mPaint.setColor(Color.parseColor("#667380")); canvas.drawLine(mRadius * 4 / 5, mRadius * 3 / 4, mRadius * 4 / 5, 2 * mRadius - (mRadius * 3 / 4), mPaint); canvas.drawLine(2 * mRadius - (mRadius * 4 / 5), mRadius * 3 / 4, 2 * mRadius - (mRadius * 4 / 5), 2 * mRadius - (mRadius * 3 / 4), mPaint); } else { int drawableInt; switch (mStatus) { case Waiting: default: drawableInt = R.mipmap.ic_waiting; break; case Pause: drawableInt = R.mipmap.ic_pause; break; case Finish: drawableInt = R.mipmap.ic_finish; break; case Error: drawableInt = R.mipmap.ic_error; break; } Drawable drawable = getContext().getResources().getDrawable(drawableInt); drawable.setBounds(0, 0, mDiameter, mDiameter); drawable.draw(canvas); } canvas.restore(); } float dp2px(Context context, float dp) { final float scale = context.getResources().getDisplayMetrics().density; return dp * scale + 0.5f; } 複製代碼
因爲對於下載更新進度的狀況來講,該控件只作狀態顯示,因此這一步不須要,要使用的話本身設置點擊事件就能夠了。
完成品效果 gif:
演示 apk 下載: blog.chengww.com/files/Circl…