HenCoder Android 自定義 View 1-5: 繪製順序

這期是 HenCoder 自定義繪製的第 1-5 期:繪製順序java

以前的內容在這裏:
HenCoder Android 開發進階 自定義 View 1-1 繪製基礎
HenCoder Android 開發進階 自定義 View 1-2 Paint 詳解
HenCoder Android 開發進階 自定義 View 1-3 文字的繪製
HenCoder Android 開發進階 自定義 View 1-4 Canvas 對繪製的輔助android

若是你沒據說過 HenCoder,能夠先看看這個:
HenCoder:給高級 Android 工程師的進階手冊git

簡介

前面幾期講的是「術」,是「用哪些 API 能夠繪製什麼內容」。到上一期爲止,「術」已經講完了,接下來要講的是「道」,是「怎麼去安排這些繪製」。github

這期是「道」的第一期:繪製順序。canvas

Android 裏面的繪製都是按順序的,先繪製的內容會被後繪製的蓋住。好比你在重疊的位置先畫圓再畫方,和先畫方再畫圓所呈現出來的結果確定是不一樣的:微信

而在實際的項目中,繪製內容相互遮蓋的狀況是很廣泛的,那麼怎麼實現本身須要的遮蓋關係,就是這期要講的內容。佈局

1 super.onDraw() 前 or 後?

前幾期我寫的自定義繪製,全都是直接繼承 View 類,而後重寫它的 onDraw() 方法,把繪製代碼寫在裏面,就像這樣:post

public class AppView extends View {
    ...

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ... // 自定義繪製代碼
    }

    ...
}複製代碼

這是自定義繪製最基本的形態:繼承 View 類,在 onDraw() 中徹底自定義它的繪製。學習

在以前的樣例中,我把繪製代碼全都寫在了 super.onDraw() 的下面。不過其實,繪製代碼寫在 super.onDraw() 的上面仍是下面都無所謂,甚至,你把 super.onDraw() 這行代碼刪掉都不要緊,效果都是同樣的——由於在 View 這個類裏,onDraw() 原本就是空實現:優化

// 在 View.java 的源碼中,onDraw() 是空的
// 因此直接繼承 View 的類,它們的 super.onDraw() 什麼也不會作
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    ...

    /** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */
    protected void onDraw(Canvas canvas) {
    }

    ...
}複製代碼

然而,除了繼承 View 類,自定義繪製更爲常見的狀況是,繼承一個具備某種功能的控件,去重寫它的 onDraw() ,在裏面添加一些繪製代碼,作出一個「進化版」的控件:

基於 EditText,在它的基礎上增長了頂部的 Hint Text 和底部的字符計數。

而這種基於已有控件的自定義繪製,就不能不考慮 super.onDraw() 了:你須要根據本身的需求,判斷出你繪製的內容須要蓋住控件原有的內容仍是須要被控件原有的內容蓋住,從而肯定你的繪製代碼是應該寫在 super.onDraw() 的上面仍是下面。

1.1 寫在 super.onDraw() 的下面

把繪製代碼寫在 super.onDraw() 的下面,因爲繪製代碼會在原有內容繪製結束以後才執行,因此繪製內容就會蓋住控件原來的內容。

這是最爲常見的狀況:爲控件增長點綴性內容。好比,在 Debug 模式下繪製出 ImageView 的圖像尺寸信息:

public class AppImageView extends ImageView {
    ...

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (DEBUG) {
            // 在 debug 模式下繪製出 drawable 的尺寸信息
            ...
        }
    }
}複製代碼

這招很好用的,試過嗎?

固然,除此以外還有其餘的不少用法,具體怎麼用就取決於你的需求、經驗和想象力了。

1.2 寫在 super.onDraw() 的上面

若是把繪製代碼寫在 super.onDraw() 的上面,因爲繪製代碼會執行在原有內容的繪製以前,因此繪製的內容會被控件的原內容蓋住。

相對來講,這種用法的場景就會少一些。不過只是少一些而不是沒有,好比你能夠經過在文字的下層繪製純色矩形來做爲「強調色」:

public class AppTextView extends TextView {
    ...

    protected void onDraw(Canvas canvas) {
        ... // 在 super.onDraw() 繪製文字以前,先繪製出被強調的文字的背景

        super.onDraw(canvas);
    }
}複製代碼

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

講了這幾期,到目前爲止我只提到了 onDraw() 這一個繪製方法。但其實繪製方法不是隻有一個的,而是有好幾個,其中 onDraw() 只是負責自身主體內容繪製的。而有的時候,你想要的遮蓋關係沒法經過 onDraw() 來實現,而是須要經過別的繪製方法。

例如,你繼承了一個 LinearLayout,重寫了它的 onDraw() 方法,在 super.onDraw() 中插入了你本身的繪製代碼,使它可以在內部繪製一些斑點做爲點綴:

public class SpottedLinearLayout extends LinearLayout {
    ...

    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       ... // 繪製斑點
    }
}複製代碼

看起來沒問題對吧?

可是你會發現,當你添加了子 View 以後,你的斑點不見了:

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

    <ImageView ... />

    <TextView ... />

</SpottedLinearLayout>複製代碼

形成這種狀況的緣由是 Android 的繪製順序:在繪製過程當中,每個 ViewGroup 會先調用本身的 onDraw() 來繪製完本身的主體以後再去繪製它的子 View。對於上面這個例子來講,就是你的 LinearLayout 會在繪製完斑點後再去繪製它的子 View。那麼在子 View 繪製完成以後,先前繪製的斑點就被子 View 蓋住了。

具體來說,這裏說的「繪製子 View」是經過另外一個繪製方法的調用來發生的,這個繪製方法叫作:dispatchDraw()。也就是說,在繪製過程當中,每一個 View 和 ViewGroup 都會先調用 onDraw() 方法來繪製主體,再調用 dispatchDraw() 方法來繪製子 View。

注:雖然 ViewViewGroup 都有 dispatchDraw() 方法,不過因爲 View 是沒有子 View 的,因此通常來講 dispatchDraw() 這個方法只對 ViewGroup(以及它的子類)有意義。

回到剛纔的問題:怎樣才能讓 LinearLayout 的繪製內容蓋住子 View 呢?只要讓它的繪製代碼在子 View 的繪製以後再執行就行了。

2.1 寫在 super.dispatchDraw() 的下面

只要重寫 dispatchDraw(),並在 super.dispatchDraw() 的下面寫上你的繪製代碼,這段繪製代碼就會發生在子 View 的繪製以後,從而讓繪製內容蓋住子 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() 以前發生,也就是繪製內容會出如今主體內容和子 View 之間。而這個……

其實和前面 1.1 講的,重寫 onDraw() 並把繪製代碼寫在 super.onDraw() 以後的作法,效果是同樣的。

能想明白爲何吧?圖就不上了。

3 繪製過程簡述

繪製過程當中最典型的兩個部分是上面講到的主體和子 View,但它們並非繪製過程的所有。除此以外,繪製過程還包含一些其餘內容的繪製。具體來說,一個完整的繪製過程會依次繪製如下幾個內容:

  1. 背景
  2. 主體(onDraw()
  3. 子 View(dispatchDraw()
  4. 滑動邊緣漸變和滑動條
  5. 前景

通常來講,一個 View(或 ViewGroup)的繪製不會這幾項全都包含,但必然逃不出這幾項,而且必定會嚴格遵照這個順序。例如一般一個 LinearLayout 只有背景和子 View,那麼它會先繪製背景再繪製子 View;一個 ImageView 有主體,有可能會再加上一層半透明的前景做爲遮罩,那麼它的前景也會在主體以後進行繪製。須要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;以前其實也有,不過只支持 FrameLayout,而直到 6.0 才把這個支持放進了 View 類裏。

這其中的第 二、3 兩步,前面已經講過了;第 1 步——背景,它的繪製發生在一個叫 drawBackground() 的方法裏,但這個方法是 private 的,不能重寫,你若是要設置背景,只能用自帶的 API 去設置(xml 佈局文件的 android:background 屬性以及 Java 代碼的 View.setBackgroundXxx() 方法,這個每一個人都用得很 6 了),而不能自定義繪製;而第 四、5 兩步——滑動邊緣漸變和滑動條以及前景,這兩部分被合在一塊兒放在了 onDrawForeground() 方法裏,這個方法是能夠重寫的。

滑動邊緣漸變和滑動條能夠經過 xml 的 android:scrollbarXXX 系列屬性或 Java 代碼的 View.setXXXScrollbarXXX() 系列方法來設置;前景能夠經過 xml 的 android:foreground 屬性或 Java 代碼的 View.setForeground() 方法來設置。而重寫 onDrawForeground() 方法,並在它的 super.onDrawForeground() 方法的上面或下面插入繪製代碼,則能夠控制繪製內容和滑動邊緣漸變、滑動條以及前景的遮蓋關係。

4 onDrawForeground()

首先,再說一遍,這個方法是 API 23 才引入的,因此在重寫這個方法的時候要確認你的 minSdk 達到了 23,否則低版本的手機裝上你的軟件會沒有效果。

onDrawForeground() 中,會依次繪製滑動邊緣漸變、滑動條和前景。因此若是你重寫 onDrawForeground()

4.1 寫在 super.onDrawForeground() 的下面

若是你把繪製代碼寫在了 super.onDrawForeground() 的下面,繪製代碼會在滑動邊緣漸變、滑動條和前景以後被執行,那麼繪製內容將會蓋住滑動邊緣漸變、滑動條和前景。

public class AppImageView extends ImageView {
    ...

    public void onDrawForeground(Canvas canvas) {
       super.onDrawForeground(canvas);

       ... // 繪製「New」標籤
    }
}複製代碼
<!-- 使用半透明的黑色做爲前景,這是一種很常見的處理 -->
<AppImageView ... android:foreground="#88000000" />複製代碼

左上角的標籤並無被黑色遮罩蓋住,而是保持了原有的顏色。

4.2 寫在 super.onDrawForeground() 的上面

若是你把繪製代碼寫在了 super.onDrawForeground() 的上面,繪製內容就會在 dispatchDraw()super.onDrawForeground() 之間執行,那麼繪製內容會蓋住子 View,但被滑動邊緣漸變、滑動條以及前景蓋住:

public class AppImageView extends ImageView {
    ...

    public void onDrawForeground(Canvas canvas) {
       ... // 繪製「New」標籤

       super.onDrawForeground(canvas);
    }
}複製代碼

因爲被半透明黑色遮罩蓋住,左上角的標籤明顯變暗了。

這種寫法,和前面 2.1 講的,重寫 dispatchDraw() 並把繪製代碼寫在 super.dispatchDraw() 的下面的效果是同樣的:繪製內容都會蓋住子 View,但被滑動邊緣漸變、滑動條以及前景蓋住。

4.3 想在滑動邊緣漸變、滑動條和前景之間插入繪製代碼?

很簡單:不行。

雖然這三部分是依次繪製的,但它們被一塊兒寫進了 onDrawForeground() 方法裏,因此你要麼把繪製內容插在它們以前,要麼把繪製內容插在它們以後。而想往它們之間插入繪製,是作不到的。

5 draw() 總調度方法

除了 onDraw() dispatchDraw()onDrawForeground() 以外,還有一個能夠用來實現自定義繪製的方法: draw()

draw() 是繪製過程的總調度方法。一個 View 的整個繪製過程都發生在 draw() 方法裏。前面講到的背景、主體、子 View 、滑動相關以及前景的繪製,它們其實都是在 draw() 方法裏的。

// View.java 的 draw() 方法的簡化版大體結構(是大體結構,不是源碼哦):

public void draw(Canvas canvas) {
    ...

    drawBackground(Canvas); // 繪製背景(不能重寫)
    onDraw(Canvas); // 繪製主體
    dispatchDraw(Canvas); // 繪製子 View
    onDrawForeground(Canvas); // 繪製滑動相關和前景

    ...
}複製代碼

從上面的代碼能夠看出,onDraw() dispatchDraw() onDrawForeground() 這三個方法在 draw() 中被依次調用,所以它們的遮蓋關係也就像前面所說的——dispatchDraw() 繪製的內容蓋住 onDraw() 繪製的內容;onDrawForeground() 繪製的內容蓋住 dispatchDraw() 繪製的內容。而在它們的外部,則是由 draw() 這個方法做爲總的調度。因此,你也能夠重寫 draw() 方法來作自定義的繪製。

5.1 寫在 super.draw() 的下面

因爲 draw() 是總調度方法,因此若是把繪製代碼寫在 super.draw() 的下面,那麼這段代碼會在其餘全部繪製完成以後再執行,也就是說,它的繪製內容會蓋住其餘的全部繪製內容。

它的效果和重寫 onDrawForeground(),並把繪製代碼寫在 super.onDrawForeground() 時的效果是同樣的:都會蓋住其餘的全部內容。

固然了,雖然說它們效果同樣,但若是你既重寫 draw() 又重寫 onDrawForeground() ,那麼 draw() 裏的內容仍是會蓋住 onDrawForeground() 裏的內容的。因此嚴格來說,它們的效果仍是有一點點不同的。

但這屬於擡槓……

5.2 寫在 super.draw() 的上面

同理,因爲 draw() 是總調度方法,因此若是把繪製代碼寫在 super.draw() 的上面,那麼這段代碼會在其餘全部繪製以前被執行,因此這部分繪製內容會被其餘全部的內容蓋住,包括背景。是的,背景也會蓋住它。

是否是以爲沒用?以爲怎麼可能會有誰想要在背景的下面繪製內容?別這麼想,有的時候它還真的有用。

例如我有一個 EditText

它下面的那條橫線,是 EditText 的背景。因此若是我想給這個 EditText 加一個綠色的底,我不能使用給它設置綠色背景色的方式,由於這就至關因而把它的背景替換掉,從而會致使下面的那條橫線消失:

<EditText ... android:background="#66BB6A" />複製代碼

EditText:我究竟是個 EditText 仍是個 TextView?傻傻分不清楚。

在這種時候,你就能夠重寫它的 draw() 方法,而後在 super.draw() 的上方插入代碼,以此來在全部內容的底部塗上一片綠色:

public AppEditText extends EditText {
    ...

    public void draw(Canvas canvas) {
        canvas.drawColor(Color.parseColor("#66BB6A")); // 塗上綠色

        super.draw(canvas);
    }
}複製代碼

固然,這種用法並不常見,事實上我也並無在項目中寫過這樣的代碼。但我想說的是,咱們做爲工程師,是沒法預知未來會遇到怎樣的需求的。咱們能作的只能是儘可能地去多學習一些、多掌握一些,儘可能地瞭解咱們可以作什麼、怎麼作,而後在需求到來的時候,就能夠多一些自如,少一些一籌莫展。

注意

關於繪製方法,有兩點須要注意一下:

  1. 出於效率的考慮,ViewGroup 默認會繞過 draw() 方法,換而直接執行 dispatchDraw(),以此來簡化繪製流程。因此若是你自定義了某個 ViewGroup 的子類(好比 LinearLayout)而且須要在它的除 dispatchDraw() 之外的任何一個繪製方法內繪製內容,你可能會須要調用 View.setWillNotDraw(false) 這行代碼來切換到完整的繪製流程(是「可能」而不是「必須」的緣由是,有些 ViewGroup 是已經調用過 setWillNotDraw(false) 了的,例如 ScrollView)。
  2. 有的時候,一段繪製代碼寫在不一樣的繪製方法中效果是同樣的,這時你能夠選一個本身喜歡或者習慣的繪製方法來重寫。但有一個例外:若是繪製代碼既能夠寫在 onDraw() 裏,也能夠寫在其餘繪製方法裏,那麼優先寫在 onDraw() ,由於 Android 有相關的優化,能夠在不須要重繪的時候自動跳過 onDraw() 的重複執行,以提高開發效率。享受這種優化的只有 onDraw() 一個方法。

總結

今天的內容就是這些:使用不一樣的繪製方法,以及在重寫的時候把繪製代碼放在 super.繪製方法() 的上面或下面不一樣的位置,以此來實現須要的遮蓋關係。下面用一張圖和一個表格總結一下:

嗯,上面這張圖在前面已經貼過了,不用比較了徹底同樣的。

另外別忘了上面提到的那兩個注意事項:

  1. ViewGroup 的子類中重寫除 dispatchDraw() 之外的繪製方法時,可能須要調用 setWillNotDraw(false)
  2. 在重寫的方法有多個選擇時,優先選擇 onDraw()

練習項目

爲了不轉頭就忘,強烈建議你趁熱打鐵,作一下這個練習項目:HenCoderPracticeDraw5

這裏放上這期練習項目的圖

下期預告

下期是「道」的第二期:動畫。

原本沒想講動畫的,由於動畫其實不屬於自定義 View 的範疇。不過最近從各個渠道的反饋裏發現有不少人對動畫的掌握都比較模糊,而動畫若是掌握得很差,自定義 View 的開發確定也會受到限制。因此好吧,增長一期動畫詳解。

順便說一下,「道」一共有三期。在這三期事後,自定義 View 的第一部分:自定義繪製就結束了。

預告圖?什麼預告圖?不存在的。

以爲贊?

若是你看完以爲有收穫,把文章轉發到你的微博、微信羣、朋友圈、公衆號,讓其餘須要的人也看到吧。

相關文章
相關標籤/搜索