Android 里面的繪制都是按順序的盒卸,先繪制的內(nèi)容會被后繪制的蓋住悠菜。比如你在重疊的位置先畫圓再畫方频轿,和先畫方再畫圓所呈現(xiàn)出來的結(jié)果肯定是不同的:
1 super.onDraw() 前 or 后逆日?
自定義繪制的最基本形態(tài):繼承 View 類余黎,在 onDraw() 中完全自定義它的繪制。
一般我們把繪制代碼全都寫在了 super.onDraw() 的下面鸟雏。不過其實隆敢,繪制代碼寫在 super.onDraw() 的上面還是下面都無所謂,甚至崔慧,你把 super.onDraw() 這行代碼刪掉都沒關(guān)系,效果都是一樣的——因為在 View 這個類里穴墅,onDraw() 本來就是空實現(xiàn)
然而惶室,除了繼承 View 類,自定義繪制更為常見的情況是玄货,繼承一個具有某種功能的控件皇钞,去重寫它的 onDraw() ,在里面添加一些繪制代碼松捉,做出一個「進化版」的控件
而這種基于已有控件的自定義繪制夹界,就不能不考慮 super.onDraw() 了:你需要根據(jù)自己的需求,判斷出你繪制的內(nèi)容需要蓋住控件原有的內(nèi)容還是需要被控件原有的內(nèi)容蓋住隘世,從而確定你的繪制代碼是應(yīng)該寫在 super.onDraw() 的上面還是下面可柿。
1.1寫在 super.onDraw() 的下面
把繪制代碼寫在 super.onDraw() 的下面,由于繪制代碼會在原有內(nèi)容繪制結(jié)束之后才執(zhí)行丙者,所以繪制內(nèi)容就會蓋住控件原來的內(nèi)容复斥。
1.2 寫在 super.onDraw() 的上面
如果把繪制代碼寫在 super.onDraw() 的上面,由于繪制代碼會執(zhí)行在原有內(nèi)容的繪制之前械媒,所以繪制的內(nèi)容會被控件的原內(nèi)容蓋住目锭。
2 dispatchDraw():繪制子 View 的方法
除了onDraw() 這一個繪制方法,其實繪制方法不是只有一個的纷捞,而是有好幾個痢虹,其中 onDraw()只是負責自身主體內(nèi)容繪制的。而有的時候主儡,你想要的遮蓋關(guān)系無法通過 onDraw() 來實現(xiàn)奖唯,而是需要通過別的繪制方法。
例如缀辩,你繼承了一個 LinearLayout臭埋,重寫了它的 onDraw() 方法踪央,在 super.onDraw() 中插入了你自己的繪制代碼,使它能夠在內(nèi)部繪制一些斑點作為點綴:
public class SpottedLinearLayout extends LinearLayout {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // 繪制斑點
}
}
但是瓢阴,當你添加了子 View 之后畅蹂,你的斑點不見了:
<SpottedLinearLayout
android:orientation="vertical"
... >
<ImageView ... />
<TextView ... />
</SpottedLinearLayout>
造成這種情況的原因是 Android 的繪制順序:在繪制過程中,每一個 ViewGroup 會先調(diào)用自己的 onDraw() 來繪制完自己的主體之后再去繪制它的子 View荣恐。對于上面這個例子來說液斜,就是你的 LinearLayout 會在繪制完斑點后再去繪制它的子 View。那么在子 View 繪制完成之后叠穆,先前繪制的斑點就被子 View 蓋住了少漆。
具體來講,這里說的「繪制子 View」是通過另一個繪制方法的調(diào)用來發(fā)生的,這個繪制方法叫做:dispatchDraw()抬旺。也就是說馁痴,在繪制過程中,每個 View 和 ViewGroup 都會先調(diào)用 onDraw() 方法來繪制主體检访,再調(diào)用 dispatchDraw() 方法來繪制子 View。
注:雖然 View 和 ViewGroup 都有 dispatchDraw() 方法仔掸,不過由于 View 是沒有子 View 的脆贵,所以一般來說 dispatchDraw() 這個方法只對 ViewGroup(以及它的子類)有意義。
怎樣才能讓 LinearLayout 的繪制內(nèi)容蓋住子 View 呢起暮?只要讓它的繪制代碼在子 View 的繪制之后再執(zhí)行就好了卖氨。
2.1 寫在 super.dispatchDraw() 的下面
只要重寫 dispatchDraw(),并在 super.dispatchDraw() 的下面寫上你的繪制代碼负懦,這段繪制代碼就會發(fā)生在子 View 的繪制之后筒捺,從而讓繪制內(nèi)容蓋住子 View 了。
public class SpottedLinearLayout extends LinearLayout {
...
// 把 onDraw() 換成了 dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
... // 繪制斑點
}
}
2.2 寫在 super.dispatchDraw() 的上面
同理密似,把繪制代碼寫在 super.dispatchDraw() 的上面焙矛,這段繪制就會在 onDraw() 之后、 super.dispatchDraw() 之前發(fā)生残腌,也就是繪制內(nèi)容會出現(xiàn)在主體內(nèi)容和子 View 之間村斟。效果和重寫 onDraw() 并把繪制代碼寫在 super.onDraw() 之后的做法是一樣的。
3 繪制過程簡述
繪制過程中最典型的兩個部分是上面講到的主體和子 View抛猫,但它們并不是繪制過程的全部蟆盹。除此之外,繪制過程還包含一些其他內(nèi)容的繪制闺金。具體來講逾滥,一個完整的繪制過程會依次繪制以下幾個內(nèi)容:
1.背景
2.主體(onDraw())
3.子View(dispatchDraw())
4.滑動邊緣漸變和滑動條
5.前景
一般來說,一個 View(或 ViewGroup)的繪制不會這幾項全都包含,但必然逃不出這幾項寨昙,并且一定會嚴格遵守這個順序讥巡。例如通常一個 LinearLayout 只有背景和子 View,那么它會先繪制背景再繪制子 View舔哪;一個 ImageView 有主體欢顷,有可能會再加上一層半透明的前景作為遮罩,那么它的前景也會在主體之后進行繪制捉蚤。需要注意抬驴,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其實也有缆巧,不過只支持 FrameLayout布持,而直到 6.0 才把這個支持放進了 View 類里。
這其中的第 2陕悬、3 兩步题暖,前面已經(jīng)講過了;第 1 步——背景捉超,它的繪制發(fā)生在一個叫 drawBackground() 的方法里芙委,但這個方法是 private 的,不能重寫狂秦,你如果要設(shè)置背景,只能用自帶的 API 去設(shè)置(xml 布局文件的 android:background 屬性以及 Java 代碼的 View.setBackgroundXxx() 方法推捐,這個每個人都用得很 6 了)裂问,而不能自定義繪制;而第 4牛柒、5 兩步——滑動邊緣漸變和滑動條以及前景堪簿,這兩部分被合在一起放在了 onDrawForeground() 方法里,這個方法是可以重寫的皮壁。
滑動邊緣漸變和滑動條可以通過 xml 的 android:scrollbarXXX 系列屬性或 Java 代碼的 View.setXXXScrollbarXXX() 系列方法來設(shè)置椭更;前景可以通過 xml 的 android:foreground 屬性或 Java 代碼的 View.setForeground() 方法來設(shè)置。而重寫 onDrawForeground() 方法蛾魄,并在它的 super.onDrawForeground() 方法的上面或下面插入繪制代碼虑瀑,則可以控制繪制內(nèi)容和滑動邊緣漸變、滑動條以及前景的遮蓋關(guān)系滴须。
4 onDrawForeground()
這個方法是 API 23 才引入的舌狗,所以在重寫這個方法的時候要確認你的 minSdk 達到了 23,不然低版本的手機裝上你的軟件會沒有效果扔水。
在 onDrawForeground() 中痛侍,會依次繪制滑動邊緣漸變、滑動條和前景
4.1 寫在 super.onDrawForeground() 的下面
如果你把繪制代碼寫在了 super.onDrawForeground() 的下面魔市,繪制代碼會在滑動邊緣漸變主届、滑動條和前景之后被執(zhí)行赵哲,那么繪制內(nèi)容將會蓋住滑動邊緣漸變、滑動條和前景君丁。
public class AppImageView extends ImageView {
...
public void onDrawForeground(Canvas canvas) {
super.onDrawForeground(canvas);
... // 繪制「New」標簽
}
}
<!-- 使用半透明的黑色作為前景枫夺,這是一種很常見的處理 -->
<AppImageView
...
android:foreground="#88000000" />
左上角的標簽并沒有被黑色遮罩蓋住,而是保持了原有的顏色谈截。
4.2 寫在 super.onDrawForeground() 的上面
如果把繪制代碼寫在了 super.onDrawForeground() 的上面筷屡,繪制內(nèi)容就會在 dispatchDraw() 和 super.onDrawForeground() 之前執(zhí)行,那么繪制內(nèi)容會蓋住子 View簸喂,但被滑動邊緣漸變毙死、滑動條以及前景蓋住:
public class AppImageView extends ImageView {
...
public void onDrawForeground(Canvas canvas) {
... // 繪制「New」標簽
super.onDrawForeground(canvas);
}
}
由于被半透明黑色遮罩蓋住喻鳄,左上角的標簽明顯變暗了扼倘。
5 draw() 總調(diào)度方法
除了 onDraw() dispatchDraw() 和 onDrawForeground() 之外,還有一個可以用來實現(xiàn)自定義繪制的方法: draw()除呵。
draw() 是繪制過程的總調(diào)度方法再菊。一個 View 的整個繪制過程都發(fā)生在 draw() 方法里。前面講到的背景颜曾、主體纠拔、子 View 、滑動相關(guān)以及前景的繪制泛豪,它們其實都是在 draw() 方法里的稠诲。
// View.java 的 draw() 方法的簡化版大致結(jié)構(gòu)(是大致結(jié)構(gòu),不是源碼)
public void draw(Canvas canvas) {
...
drawBackground(Canvas); // 繪制背景(不能重寫)
onDraw(Canvas); // 繪制主體
dispatchDraw(Canvas); // 繪制子 View
onDrawForeground(Canvas); // 繪制滑動相關(guān)和前景
...
}
從上面的代碼可以看出诡曙,onDraw() dispatchDraw() onDrawForeground() 這三個方法在 draw() 中被依次調(diào)用臀叙,因此它們的遮蓋關(guān)系也就像前面所說的——dispatchDraw() 繪制的內(nèi)容蓋住 onDraw() 繪制的內(nèi)容;onDrawForeground() 繪制的內(nèi)容蓋住 dispatchDraw() 繪制的內(nèi)容价卤。而在它們的外部劝萤,則是由 draw() 這個方法作為總的調(diào)度。所以慎璧,你也可以重寫 draw() 方法來做自定義的繪制床嫌。
5.1 寫在 super.draw() 的下面
由于 draw() 是總調(diào)度方法,所以如果把繪制代碼寫在 super.draw() 的下面胸私,那么這段代碼會在其他所有繪制完成之后再執(zhí)行既鞠,也就是說,它的繪制內(nèi)容會蓋住其他的所有繪制內(nèi)容盖文。
5.2 寫在 super.draw() 的上面
同理嘱蛋,由于 draw() 是總調(diào)度方法,所以如果把繪制代碼寫在 super.draw() 的上面,那么這段代碼會在其他所有繪制之前被執(zhí)行洒敏,所以這部分繪制內(nèi)容會被其他所有的內(nèi)容蓋住龄恋,包括背景。是的凶伙,背景也會蓋住它郭毕。
注意
關(guān)于繪制方法,有兩點需要注意一下:
- 出于效率的考慮函荣,ViewGroup 默認會繞過 draw() 方法显押,換而直接執(zhí)行 dispatchDraw(),以此來簡化繪制流程傻挂。所以如果你自定義了某個 ViewGroup 的子類(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一個繪制方法內(nèi)繪制內(nèi)容乘碑,你可能會需要調(diào)用 View.setWillNotDraw(false) 這行代碼來切換到完整的繪制流程(是「可能」而不是「必須」的原因是,有些 ViewGroup 是已經(jīng)調(diào)用過 setWillNotDraw(false) 了的金拒,例如 ScrollView)兽肤。
- 有的時候,一段繪制代碼寫在不同的繪制方法中效果是一樣的绪抛,這時你可以選一個自己喜歡或者習(xí)慣的繪制方法來重寫资铡。但有一個例外:如果繪制代碼既可以寫在 onDraw() 里,也可以寫在其他繪制方法里幢码,那么優(yōu)先寫在 onDraw() 笤休,因為 Android 有相關(guān)的優(yōu)化,可以在不需要重繪的時候自動跳過 onDraw() 的重復(fù)執(zhí)行症副,以提升開發(fā)效率宛官。享受這種優(yōu)化的只有 onDraw() 一個方法。