自定義控件-繪制順序

Android 里面的繪制都是按順序的,先繪制的內(nèi)容會(huì)被后繪制的蓋住。比如你在重疊的位置先畫圓再畫方,和先畫方再畫圓所呈現(xiàn)出來(lái)的結(jié)果肯定是不同的:


繪制順序

1 super.onDraw() 前 or 后?

自定義繪制的最基本形態(tài):繼承 View 類,在 onDraw() 中完全自定義它的繪制。

一般我們把繪制代碼全都寫在了 super.onDraw() 的下面。不過(guò)其實(shí),繪制代碼寫在 super.onDraw() 的上面還是下面都無(wú)所謂,甚至,你把 super.onDraw() 這行代碼刪掉都沒(méi)關(guān)系,效果都是一樣的——因?yàn)樵?View 這個(gè)類里,onDraw() 本來(lái)就是空實(shí)現(xiàn)

然而,除了繼承 View 類,自定義繪制更為常見(jiàn)的情況是,繼承一個(gè)具有某種功能的控件,去重寫它的 onDraw() ,在里面添加一些繪制代碼,做出一個(gè)「進(jìn)化版」的控件

而這種基于已有控件的自定義繪制,就不能不考慮 super.onDraw() 了:你需要根據(jù)自己的需求,判斷出你繪制的內(nèi)容需要蓋住控件原有的內(nèi)容還是需要被控件原有的內(nèi)容蓋住,從而確定你的繪制代碼是應(yīng)該寫在 super.onDraw() 的上面還是下面。

1.1寫在 super.onDraw() 的下面

把繪制代碼寫在 super.onDraw() 的下面,由于繪制代碼會(huì)在原有內(nèi)容繪制結(jié)束之后才執(zhí)行,所以繪制內(nèi)容就會(huì)蓋住控件原來(lái)的內(nèi)容。

1.2 寫在 super.onDraw() 的上面

如果把繪制代碼寫在 super.onDraw() 的上面,由于繪制代碼會(huì)執(zhí)行在原有內(nèi)容的繪制之前,所以繪制的內(nèi)容會(huì)被控件的原內(nèi)容蓋住。

2 dispatchDraw():繪制子 View 的方法

除了onDraw() 這一個(gè)繪制方法,其實(shí)繪制方法不是只有一個(gè)的,而是有好幾個(gè),其中 onDraw()只是負(fù)責(zé)自身主體內(nèi)容繪制的。而有的時(shí)候,你想要的遮蓋關(guān)系無(wú)法通過(guò) onDraw() 來(lái)實(shí)現(xiàn),而是需要通過(guò)別的繪制方法。

例如,你繼承了一個(gè) LinearLayout,重寫了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的繪制代碼,使它能夠在內(nèi)部繪制一些斑點(diǎn)作為點(diǎn)綴:

public class SpottedLinearLayout extends LinearLayout {
    ...
    
    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
    
       ... // 繪制斑點(diǎn)
    }
}
onDraw()

但是,當(dāng)你添加了子 View 之后,你的斑點(diǎn)不見(jiàn)了:

<SpottedLinearLayout
    android:orientation="vertical"
    ... >

    <ImageView ... />

    <TextView ... />

</SpottedLinearLayout>

造成這種情況的原因是 Android 的繪制順序:在繪制過(guò)程中,每一個(gè) ViewGroup 會(huì)先調(diào)用自己的 onDraw() 來(lái)繪制完自己的主體之后再去繪制它的子 View。對(duì)于上面這個(gè)例子來(lái)說(shuō),就是你的 LinearLayout 會(huì)在繪制完斑點(diǎn)后再去繪制它的子 View。那么在子 View 繪制完成之后,先前繪制的斑點(diǎn)就被子 View 蓋住了。
具體來(lái)講,這里說(shuō)的「繪制子 View」是通過(guò)另一個(gè)繪制方法的調(diào)用來(lái)發(fā)生的,這個(gè)繪制方法叫做:dispatchDraw()。也就是說(shuō),在繪制過(guò)程中,每個(gè) View 和 ViewGroup 都會(huì)先調(diào)用 onDraw() 方法來(lái)繪制主體,再調(diào)用 dispatchDraw() 方法來(lái)繪制子 View。

注:雖然 View 和 ViewGroup 都有 dispatchDraw() 方法,不過(guò)由于 View 是沒(méi)有子 View 的,所以一般來(lái)說(shuō) dispatchDraw() 這個(gè)方法只對(duì) ViewGroup(以及它的子類)有意義。

怎樣才能讓 LinearLayout 的繪制內(nèi)容蓋住子 View 呢?只要讓它的繪制代碼在子 View 的繪制之后再執(zhí)行就好了。

2.1 寫在 super.dispatchDraw() 的下面

只要重寫 dispatchDraw(),并在 super.dispatchDraw() 的下面寫上你的繪制代碼,這段繪制代碼就會(huì)發(fā)生在子 View 的繪制之后,從而讓繪制內(nèi)容蓋住子 View 了。

public class SpottedLinearLayout extends LinearLayout {
    ...
    
    // 把 onDraw() 換成了 dispatchDraw()
    protected void dispatchDraw(Canvas canvas) {
       super.dispatchDraw(canvas);
    
       ... // 繪制斑點(diǎn)
    }

}

2.2 寫在 super.dispatchDraw() 的上面

同理,把繪制代碼寫在 super.dispatchDraw() 的上面,這段繪制就會(huì)在 onDraw() 之后、 super.dispatchDraw() 之前發(fā)生,也就是繪制內(nèi)容會(huì)出現(xiàn)在主體內(nèi)容和子 View 之間。效果和重寫 onDraw() 并把繪制代碼寫在 super.onDraw() 之后的做法是一樣的。

3 繪制過(guò)程簡(jiǎn)述

繪制過(guò)程中最典型的兩個(gè)部分是上面講到的主體和子 View,但它們并不是繪制過(guò)程的全部。除此之外,繪制過(guò)程還包含一些其他內(nèi)容的繪制。具體來(lái)講,一個(gè)完整的繪制過(guò)程會(huì)依次繪制以下幾個(gè)內(nèi)容:
1.背景
2.主體(onDraw())
3.子View(dispatchDraw())
4.滑動(dòng)邊緣漸變和滑動(dòng)條
5.前景

一般來(lái)說(shuō),一個(gè) View(或 ViewGroup)的繪制不會(huì)這幾項(xiàng)全都包含,但必然逃不出這幾項(xiàng),并且一定會(huì)嚴(yán)格遵守這個(gè)順序。例如通常一個(gè) LinearLayout 只有背景和子 View,那么它會(huì)先繪制背景再繪制子 View;一個(gè) ImageView 有主體,有可能會(huì)再加上一層半透明的前景作為遮罩,那么它的前景也會(huì)在主體之后進(jìn)行繪制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其實(shí)也有,不過(guò)只支持 FrameLayout,而直到 6.0 才把這個(gè)支持放進(jìn)了 View 類里。

這其中的第 2、3 兩步,前面已經(jīng)講過(guò)了;第 1 步——背景,它的繪制發(fā)生在一個(gè)叫 drawBackground() 的方法里,但這個(gè)方法是 private 的,不能重寫,你如果要設(shè)置背景,只能用自帶的 API 去設(shè)置(xml 布局文件的 android:background 屬性以及 Java 代碼的 View.setBackgroundXxx() 方法,這個(gè)每個(gè)人都用得很 6 了),而不能自定義繪制;而第 4、5 兩步——滑動(dòng)邊緣漸變和滑動(dòng)條以及前景,這兩部分被合在一起放在了 onDrawForeground() 方法里,這個(gè)方法是可以重寫的。


繪制順序

滑動(dòng)邊緣漸變和滑動(dòng)條可以通過(guò) xml 的 android:scrollbarXXX 系列屬性或 Java 代碼的 View.setXXXScrollbarXXX() 系列方法來(lái)設(shè)置;前景可以通過(guò) xml 的 android:foreground 屬性或 Java 代碼的 View.setForeground() 方法來(lái)設(shè)置。而重寫 onDrawForeground() 方法,并在它的 super.onDrawForeground() 方法的上面或下面插入繪制代碼,則可以控制繪制內(nèi)容和滑動(dòng)邊緣漸變、滑動(dòng)條以及前景的遮蓋關(guān)系。

4 onDrawForeground()

這個(gè)方法是 API 23 才引入的,所以在重寫這個(gè)方法的時(shí)候要確認(rèn)你的 minSdk 達(dá)到了 23,不然低版本的手機(jī)裝上你的軟件會(huì)沒(méi)有效果。

在 onDrawForeground() 中,會(huì)依次繪制滑動(dòng)邊緣漸變、滑動(dòng)條和前景

4.1 寫在 super.onDrawForeground() 的下面

如果你把繪制代碼寫在了 super.onDrawForeground() 的下面,繪制代碼會(huì)在滑動(dòng)邊緣漸變、滑動(dòng)條和前景之后被執(zhí)行,那么繪制內(nèi)容將會(huì)蓋住滑動(dòng)邊緣漸變、滑動(dòng)條和前景。

public class AppImageView extends ImageView {
    ...
    
    public void onDrawForeground(Canvas canvas) {
       super.onDrawForeground(canvas);
    
       ... // 繪制「New」標(biāo)簽
    }

}
<!-- 使用半透明的黑色作為前景,這是一種很常見(jiàn)的處理 -->
<AppImageView
    ...
    android:foreground="#88000000" />
寫在 super.onDrawForeground() 的下面

左上角的標(biāo)簽并沒(méi)有被黑色遮罩蓋住,而是保持了原有的顏色。

4.2 寫在 super.onDrawForeground() 的上面

如果把繪制代碼寫在了 super.onDrawForeground() 的上面,繪制內(nèi)容就會(huì)在 dispatchDraw() 和 super.onDrawForeground() 之前執(zhí)行,那么繪制內(nèi)容會(huì)蓋住子 View,但被滑動(dòng)邊緣漸變、滑動(dòng)條以及前景蓋?。?/p>

public class AppImageView extends ImageView {
    ...
    
    public void onDrawForeground(Canvas canvas) {
       ... // 繪制「New」標(biāo)簽
    
       super.onDrawForeground(canvas);
    }
}
寫在 super.onDrawForeground() 的上面

由于被半透明黑色遮罩蓋住,左上角的標(biāo)簽明顯變暗了。

5 draw() 總調(diào)度方法

除了 onDraw() dispatchDraw() 和 onDrawForeground() 之外,還有一個(gè)可以用來(lái)實(shí)現(xiàn)自定義繪制的方法: draw()。

draw() 是繪制過(guò)程的總調(diào)度方法。一個(gè) View 的整個(gè)繪制過(guò)程都發(fā)生在 draw() 方法里。前面講到的背景、主體、子 View 、滑動(dòng)相關(guān)以及前景的繪制,它們其實(shí)都是在 draw() 方法里的。

// View.java 的 draw() 方法的簡(jiǎn)化版大致結(jié)構(gòu)(是大致結(jié)構(gòu),不是源碼)

public void draw(Canvas canvas) {
    ...
    
    drawBackground(Canvas); // 繪制背景(不能重寫)
    onDraw(Canvas); // 繪制主體
    dispatchDraw(Canvas); // 繪制子 View
    onDrawForeground(Canvas); // 繪制滑動(dòng)相關(guān)和前景
    
    ...
}

從上面的代碼可以看出,onDraw() dispatchDraw() onDrawForeground() 這三個(gè)方法在 draw() 中被依次調(diào)用,因此它們的遮蓋關(guān)系也就像前面所說(shuō)的——dispatchDraw() 繪制的內(nèi)容蓋住 onDraw() 繪制的內(nèi)容;onDrawForeground() 繪制的內(nèi)容蓋住 dispatchDraw() 繪制的內(nèi)容。而在它們的外部,則是由 draw() 這個(gè)方法作為總的調(diào)度。所以,你也可以重寫 draw() 方法來(lái)做自定義的繪制。

整體調(diào)度

5.1 寫在 super.draw() 的下面

由于 draw() 是總調(diào)度方法,所以如果把繪制代碼寫在 super.draw() 的下面,那么這段代碼會(huì)在其他所有繪制完成之后再執(zhí)行,也就是說(shuō),它的繪制內(nèi)容會(huì)蓋住其他的所有繪制內(nèi)容。

5.2 寫在 super.draw() 的上面

同理,由于 draw() 是總調(diào)度方法,所以如果把繪制代碼寫在 super.draw() 的上面,那么這段代碼會(huì)在其他所有繪制之前被執(zhí)行,所以這部分繪制內(nèi)容會(huì)被其他所有的內(nèi)容蓋住,包括背景。是的,背景也會(huì)蓋住它。

注意

關(guān)于繪制方法,有兩點(diǎn)需要注意一下:

  1. 出于效率的考慮,ViewGroup 默認(rèn)會(huì)繞過(guò) draw() 方法,換而直接執(zhí)行 dispatchDraw(),以此來(lái)簡(jiǎn)化繪制流程。所以如果你自定義了某個(gè) ViewGroup 的子類(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一個(gè)繪制方法內(nèi)繪制內(nèi)容,你可能會(huì)需要調(diào)用 View.setWillNotDraw(false) 這行代碼來(lái)切換到完整的繪制流程(是「可能」而不是「必須」的原因是,有些 ViewGroup 是已經(jīng)調(diào)用過(guò) setWillNotDraw(false) 了的,例如 ScrollView)。
  2. 有的時(shí)候,一段繪制代碼寫在不同的繪制方法中效果是一樣的,這時(shí)你可以選一個(gè)自己喜歡或者習(xí)慣的繪制方法來(lái)重寫。但有一個(gè)例外:如果繪制代碼既可以寫在 onDraw() 里,也可以寫在其他繪制方法里,那么優(yōu)先寫在 onDraw() ,因?yàn)?Android 有相關(guān)的優(yōu)化,可以在不需要重繪的時(shí)候自動(dòng)跳過(guò) onDraw() 的重復(fù)執(zhí)行,以提升開(kāi)發(fā)效率。享受這種優(yōu)化的只有 onDraw() 一個(gè)方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容