基于 Android 系統(tǒng)視圖繪制原理與事件分發(fā)機(jī)制我們可以構(gòu)造出系統(tǒng)組件之外的視圖類以滿足特定產(chǎn)品需求,這是一個(gè)龐大但過程明確的體系,本文從實(shí)踐出發(fā),通過實(shí)現(xiàn)一個(gè)圓形進(jìn)度視圖介紹怎樣使用 Paint 工具在 View 的 onDraw 階段繪制出想要的自定義 View 以及這其中的思考方法與最佳實(shí)踐,最后得到打包后僅8KB但功能強(qiáng)大的 View 庫。
Github 源碼地址:https://github.com/timqi/SectorProgressView


為實(shí)現(xiàn)某個(gè)特定的視圖效果通常需要先對它進(jìn)行分解,分解的足夠細(xì)以至于和已有的工具(如SDK API)完美對接在一起則離實(shí)現(xiàn)目標(biāo)就完成一大半了。
帶著分解的思路我們首先看第一個(gè)示例 SectorProgressView。雖然很簡單,但我們?nèi)匀豢梢杂胁煌姆纸夥椒?,比?/p>
- 先用背景色畫一個(gè)圓
- 再用前景色根據(jù)進(jìn)度繪制扇形區(qū)域
或者:
- 使用前景色繪制出表示進(jìn)度的部分
- 使用背景色繪制剩余扇形部分
上面兩種分解思路都能輕松的實(shí)現(xiàn)目的,但是比較其中異同我們發(fā)現(xiàn)后一種方法相較前一種方法避免了表示進(jìn)度的前景色部分的重繪,這在某些高性能要求的情況下是要考慮的,當(dāng)然今天這種簡單的控件我們選擇第一種方案。
要實(shí)現(xiàn)這個(gè)方案我們需要拿到繪制所需的參數(shù),接下來就要分析需要哪些參數(shù)來畫背景圓,哪些參數(shù)來畫扇形進(jìn)度區(qū)域:
- 背景色值
- 前景色值
- 進(jìn)度的百分比的值
- 進(jìn)度區(qū)域開始時(shí)的角度
- 描述圓位置的矩形區(qū)域
要拿到上面描述的參數(shù),結(jié)合 Android SDK 提供的方法我們可以在構(gòu)造函數(shù),onSizeChanged 中獲得,于是編寫代碼:
public class SectorProgressView extends View {
private int bgColor;
private int fgColor;
private float percent;
private float startAngle;
private RectF oval;
private ObjectAnimator animator;
public SectorProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.SectorProgressView,
0, 0);
try {
bgColor = a.getColor(R.styleable.SectorProgressView_bgColor, 0xffe5e5e5);
fgColor = a.getColor(R.styleable.SectorProgressView_fgColor, 0xffff765c);
percent = a.getFloat(R.styleable.SectorProgressView_percent, 0);
startAngle = a.getFloat(R.styleable.SectorProgressView_startAngle, 0) + 270;
} finally {
a.recycle();
}
init();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
float xpad = (float) (getPaddingLeft() + getPaddingRight());
float ypad = (float) (getPaddingBottom() + getPaddingTop());
float wwd = (float) w - xpad;
float hhd = (float) h - ypad;
oval = new RectF(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + wwd, getPaddingTop() + hhd);
}
private void refreshTheLayout() {
invalidate();
requestLayout();
}
...
}
繼承 View 類編寫構(gòu)造函數(shù),監(jiān)聽 onSizeChanged 方法以獲取我們需要的參數(shù)。同時(shí)為這些參數(shù)添加 getter,setter 方法,在 setter 方法中調(diào)用 refreshTheLayout 觸發(fā)繪制以及時(shí)看到效果,最后在 onDraw 函數(shù)中繪制圖形
private void init() {
bgPaint = new Paint();
bgPaint.setColor(bgColor);
fgPaint = new Paint();
fgPaint.setColor(fgColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(oval, 0, 360, true, bgPaint);
canvas.drawArc(oval, startAngle, percent * 3.6f, true, fgPaint);
}
onDraw 方法非常簡單,我們也應(yīng)該在所有的情況下保證 onDraw 方法足夠簡單,甚至包括精簡申請初始化變量這種操作,盡可能減少 CPU 世間與內(nèi)存占用。
通常我們認(rèn)為 Android 手機(jī)以 60fps 的幀率運(yùn)行是流暢的,也就是說手機(jī)屏幕要每秒刷新 60 次。也就是要想保證 60fps 的幀率需要重繪的所有操作在 16ms 內(nèi)完成。這些操作不僅包括了當(dāng)前 View 的 onDraw 方法,還有其他 View 的,還包括一些布局等計(jì)算,所以我們應(yīng)該盡可能保證 onDraw 方法足夠簡單。
最后為視圖添加一個(gè)無限循環(huán)的動(dòng)畫。動(dòng)畫本質(zhì)即是一系列繪制屬性關(guān)于時(shí)間的函數(shù),進(jìn)度無限循環(huán)的動(dòng)畫就是 startAngle 屬性在時(shí)間上連續(xù)不斷改變的結(jié)果。同時(shí) Android SDK 也提供了很多用于構(gòu)建動(dòng)畫的類,比如 ObjectAnimator,雖然 startAngle 是一個(gè)自定義的屬性,但是受益于 ObjectAnimator 使用反射的靈活,為 startAngle 提供 getter,setter 方法后依然可以使用 ObjectAnimator。
public void animateIndeterminate(int durationOneCircle,
TimeInterpolator interpolator) {
animator = ObjectAnimator.ofFloat(this, "startAngle", getStartAngle(), getStartAngle() + 360);
if (interpolator != null) animator.setInterpolator(interpolator);
animator.setDuration(durationOneCircle);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.RESTART);
animator.start();
}
對于 ColorfulRingProgressView 同樣適用上面的思路
繪制分解 -> 分析所需參數(shù) -> 獲取參數(shù) -> draw
當(dāng)然,分析的步驟需要了解 Framework 已經(jīng)為我們提供了什么功能,比如 Paint,Canvas。熟悉已有的高效實(shí)現(xiàn)的 API 有助于我們快速構(gòu)建優(yōu)質(zhì)代碼。