在前面的系列文章之后,大家已經(jīng)對(duì) View 繪制中的各個(gè)過程熟悉了,這篇文章就根據(jù)前面所學(xué)的知識(shí),來實(shí)際動(dòng)手做一個(gè)自定義 View,這也是整個(gè)系列的完結(jié)篇了,會(huì)盡可能地涵蓋自定義 View 中的各個(gè)方面,以及可能遇到的各種坑。在經(jīng)過一番考量后,決定使用一個(gè) PagerIndicator 作為例子進(jìn)行講解。在開始后面的文章之前,建議 Clone 下 https://github.com/woaitqs/FPageIndicator,對(duì)照著源碼閱讀,食用效果更佳。
最后我們需要實(shí)現(xiàn)的效果如圖所示:

確定自定義的樣式
一般情況下,我們需要自定義 View 的時(shí)候,都要考量有哪些地方需要自定義的,對(duì)于上圖的情況,可以認(rèn)為選中和未選中的顏色是可以自定義的,大圈和小圈的自定義也是可以自定義的,還有諸如間距等等這些也是可以的。為了使開發(fā)者在使用的時(shí)候,能夠在 XML 文件中也能進(jìn)行自定義,我們需要聲明 styleable 文件,官方教程 有詳細(xì)說明如何進(jìn)行屬性定義。
對(duì)于上圖的例子,可以寫如下的styleable 文件。建議自定義的屬性上,添加上同一的前綴,這樣更容易區(qū)分開來。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PageIndicator">
<attr name="pi_count" format="integer" />
<attr name="pi_out_radius" format="dimension" />
<attr name="pi_radius" format="dimension" />
<attr name="pi_un_focus_color" format="color"/>
<attr name="pi_focus_color" format="color"/>
<attr name="pi_padding" format="dimension"/>
</declare-styleable>
</resources>
那么如何在自定義的 View 中,將這些設(shè)置進(jìn)去的屬性取出來呢?這時(shí)候我們要用到 TypedArray,通過這個(gè)能屬性從 AttributeSet 取出來,同時(shí)也需要注意在用戶沒有設(shè)置自定義屬性時(shí)的默認(rèn)值。代碼也相對(duì)很簡單,如下所示。
private void initAttributes(AttributeSet attrs) {
if (attrs == null) {
return;
}
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PageIndicator);
count = typedArray.getInteger(R.styleable.PageIndicator_pi_count, 1);
outRadius = typedArray.getDimension(R.styleable.PageIndicator_pi_out_radius,
dpToPx(DEFAULT_OUT_RADIUS_SIZE));
innerRadius = typedArray.getDimension(R.styleable.PageIndicator_pi_radius,
dpToPx(DEFAULT_INNER_RADIUS_SIZE));
unFocusColor = typedArray.getColor(R.styleable.PageIndicator_pi_un_focus_color,
Color.parseColor(DEFAULT_UN_FOCUS_COLOR));
focusColor = typedArray.getColor(R.styleable.PageIndicator_pi_focus_color,
Color.parseColor(DEFAULT_FOCUS_COLOR));
padding = typedArray.getDimension(R.styleable.PageIndicator_pi_padding, 0F);
typedArray.recycle();
}
初始化相應(yīng)屬性
在構(gòu)造函數(shù)或者 attachToWindow 的時(shí)候,就初始化自定義 View 中可能用到的各種變量,例如 Paint,而不是在使用時(shí),再去構(gòu)造。因?yàn)橹T如 onMeasure、onDraw 方法可能會(huì)被多次調(diào)用,頻繁地分配對(duì)象,可能會(huì)引起內(nèi)存抖動(dòng)。內(nèi)存抖動(dòng)會(huì)在短時(shí)間內(nèi)觸發(fā)多次 GC 操作,從而引起卡頓。我在這篇文章中,http://www.woaitqs.cc/2016/03/30/in-love-with-android-memory 說明了這種情況是如何發(fā)生的。
對(duì)于本文的情況,初始化 Paint 就可以了。
private void initPaint() {
innerUnFocusPaint.setStyle(Paint.Style.FILL);
innerUnFocusPaint.setColor(unFocusColor);
innerFocusPaint.setStyle(Paint.Style.FILL);
innerFocusPaint.setColor(focusColor);
outPaint.setStyle(Paint.Style.STROKE);
outPaint.setColor(focusColor);
outPaint.setStrokeWidth(2F);
}
重載 onMeasure 方法
onMeasure 方法是讓 view 自身測(cè)量自己的大小,這是關(guān)鍵步驟。分為兩種情況,繼承自 ViewGroup 的,還是繼承自 View 。如果是 ViewGroup 的話,需要在合適的地方,讓子 View 測(cè)量后,再得到自身的大小。而如果是 View,則只需管好自己的大小即可。
MeasureSpec 是 onMeasure 方法中參數(shù)的類型,它通過位運(yùn)算的方式,涵蓋了模式和大小兩個(gè)數(shù)據(jù),通過 getMode 和 getSize 可以分別得到模式和大小。這是要特別針對(duì)模式進(jìn)行說明,如果模式為 EXACTLY,說明 View 的大小是指定了的,那么就使用傳入的指即可。如果是 AT_MOST, 那么就得先計(jì)算出預(yù)期的大小。預(yù)期的大小,是指在不考慮外界的情況下,View 所占據(jù)的大小,對(duì)于本文的例子,預(yù)期的大小就是幾個(gè)圈的大小加上它們之間的間距。在計(jì)算之后,與指定的大小取兩者最小的。如果是 UNSPECIFIED 的情況,那說明父類對(duì)你的大小沒有預(yù)期,那就使用預(yù)期的大小即可。下面的代碼是一個(gè)很清晰的說明。
最后一定不要忘記,調(diào)用 setDimension 方法哦。
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
更多知識(shí),參考 http://www.woaitqs.cc/2016/10/18/android-view-theory-2.
重載 onDraw 方法
onDraw 方法就更為簡單了,只是簡單地對(duì) Canvas 提供的 API 進(jìn)行調(diào)用。在調(diào)用時(shí),一定要對(duì) Android 的坐標(biāo)系非常清楚才行,知道哪里是 X 軸,哪里是 Y 軸,left,right,top 和 bottom 是相對(duì)于哪里的距離。

只要計(jì)算好每個(gè)繪制 View 的坐標(biāo),再調(diào)用相應(yīng)的 API 就能看到效果了!Cheers!下面的代碼,就繪制了選中和未選中的幾個(gè)點(diǎn)。再強(qiáng)調(diào)下,這里使用的 Paint 是提前初始化完成的哦。
for (int i = 0; i < count; i++) {
canvas.drawCircle(
outRadius - innerRadius + padding * i + innerRadius * (1 + 2 * i),
height / 2,
innerRadius,
i == selectedPos ? innerFocusPaint : innerUnFocusPaint);
}
在自定義 onDraw 的時(shí)候,設(shè)計(jì)的宗旨是保持無狀態(tài)性,也就是說,onDraw 是對(duì)每一個(gè)時(shí)刻狀態(tài)的投訴,例如本例子,就應(yīng)該是某個(gè)滑動(dòng)時(shí)刻的投射。如果在 onDraw 中進(jìn)行動(dòng)畫,或者起一些異步線程等等,會(huì)使得代碼的可讀性變差。
View 的更新策略
寫完自定義 View 后,還得在用戶更改屬性后或者其他事件后,相應(yīng)地讓 View 更新下。
requestLayout.
會(huì)觸發(fā) ViewRoot 的 performTraversal方法,從而重新執(zhí)行 Measure、Layout,在特定情況下會(huì)觸發(fā) Draw 操作。
invalidate.
會(huì)觸發(fā) Draw 操作,如果 View 的大小和位置沒有發(fā)生改變,調(diào)用這個(gè)方法就足以更新頁面了。
在本文的例子里面,當(dāng)用戶滑動(dòng) ViewPager 時(shí),View 的大小和位置不會(huì)發(fā)生改變,因而調(diào)用 invalidate 就足夠了。
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
drawPosition = position;
drawPositionOffset = positionOffset;
invalidate();
}
至此,整個(gè)系列結(jié)束,有興趣的同學(xué),可以 Clone https://github.com/woaitqs/FPageIndicator這個(gè)項(xiàng)目來進(jìn)行探討 :)。
文檔信息
- 版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名(創(chuàng)意共享3.0許可證)
- 發(fā)表日期:2016年11月28日
- 社交媒體:weibo.com/woaitqs