自定義 view - 3大核心方法補充

自定義 view 的3個核心方法

  • onMeasure
    根據 view 的測量模式計算確定 view 的寬高
  • onLayout
    ViewGroup 中對所有的子 view 排版,決定子 view 的位置
  • onDraw
    具體繪制 view

要繼續了解的點

我們在了解了自定義 view 的3大核心方法后就完了嗎,沒有啊,這只是開始呢,3大核心方法中還有一些要補充的點,這些我們清楚之后,會在今后的開發中幫助我們理順思路:

  • view 的寬高生命周期內的變化
  • onSizeChange 方法是不是一定會執行
  • view 的位移,位置變化會觸發 view 自身的那些函數
  • ViewGroup 會執行2次 onMeasure ,2次 onLayout ,1次 onDraw
  • 自定義的ViewGroup 的 setWillNotDraw(false)

ViewGroup 的繪制

在自定義 ViewGroup 中,我們需要 setWillNotDraw(false),才能執行 ViewGroup 自身的繪制方法

ViewGroup 的繪制過程如下:

public void draw(Canvas canvas) {
  . . . 
  // 繪制背景,只有dirtyOpaque為false時才進行繪制,下同
  int saveCount;
  if (!dirtyOpaque) {
    drawBackground(canvas);
  }

  . . . 

  // 繪制自身內容
  if (!dirtyOpaque) onDraw(canvas);

  // 繪制子View
  dispatchDraw(canvas);

   . . .
  // 繪制滾動條等
  onDrawForeground(canvas);

}

看見沒有 ViewGroup 先繪制的背景,再繪制自身,最后遍歷繪制子 view


view 的寬高生命周期內的變化

view 自身大小的失計算問題其實涉及 view 的 onMeasure 和 onSizeChange 方法,我們要在 view 的真個生命周期范圍內觀察 view 寬高的變化

重新回顧一下,view 的寬高涉及到2套 API:

  • getWidth() / getHeight():獲得View最終的寬 / 高
  • getMeasuredWidth() / getMeasuredHeight():獲得 View測量的寬 / 高

然后我們設計一個自定義 view 然后打印一下在 view 的各個生命周期方法中 寬高的數值

自定義 view

public class MyView2 extends View {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        Log.d("AAA", "onMeasure /" + " widthSize = " + widthSize + " , heightSize = " + heightSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        Log.d("AAA", "onSizeChanged / w = " + w + " , h = " + h + " , oldw = " + oldw + " , oldh = " + oldh);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        Log.d("AAA", "onLayout / measureWidth = " + getMeasuredWidth() + " , measureHeight = " + getMeasuredHeight() + " , width = " + getWidth() + " , height = " + getHeight());
    }

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

        Log.d("AAA", "onLayout / measureWidth = " + getMeasuredWidth() + " , measureHeight = " + getMeasuredHeight() + " , width = " + getWidth() + " , height = " + getHeight());

    }
}

然后使用一個最簡單的布局方案來觀察

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.bloodcrown.aaa02.MeasureActivity">

        <com.bloodcrown.aaa02.MyView2
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

</FrameLayout>

結果:


Snip20180627_4.png

從結果來看:

  • onMeasure 方法執行多次,其中計算出來的寬高值都可能會改變
  • onMeasure 方法中 getWidth、getHeight 方法的返回值都是0
  • view 寬高的改變觸發了 onSizeChange 方法,onSizeChange 方法在 onLayout 之前執行
  • onLayout 方法中2套 API 的取值都是一樣的,在這個時候 getWidth、getHeight 已經可以獲取到具體的返回值

然后有一個問題,這里 onSizeChange 方法的執行是因為 onMeasure 執行了2次,view 的大小發生了改變。那么若是view 的大小不改變,那么 onSizeChange 還有機會執行嗎

上面的例子中,自定義 view 使用了 match_parent 計算方法引起了 view 大小的變化,那么我們給一個具體值呢。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.bloodcrown.aaa02.MeasureActivity">

        <com.bloodcrown.aaa02.MyView2
            android:layout_width="200dp"
            android:layout_height="200dp"/>

</FrameLayout>

結果:


Snip20180627_5.png

view 的大小沒有變化,onSizeChange 方法 一樣執行了,其實很好理解,view 的會記錄自己的大小,在 view 初始化時 view 的大小是 0啊,所以 onSizeChange 方法是一定會執行的,至少會執行一次


view 的坐標屬性變化和位移會觸發 view 的哪些生命周期方法

1. UI 重繪方法

UI 重繪方法有下面幾個方法:

  • invalidate()
    重繪視圖,必須 Ui 線程執行
  • postInvalidate()
    重繪視圖,使用 handle 消息機制允許異步執行
  • requestLayout()
    重新布局
  • requestFocus
    局部刷新,只重繪焦點部分或是我們指定的部分

然后我們來看下這幾個方法會觸發哪幾個 view 的函數

  • invalidate


    Snip20180628_3.png

    這么看的話重繪真的是重繪,只會觸發 view 的繪制方法

  • postInvalidate


    Snip20180628_5.png

    一樣只會觸發 view 的繪制方法,看來 postInvalidate 和 invalidate 區別的地方只是支不支持異步執行

  • requestLayout


    Snip20180628_2.png

    計算,布局,繪制 3個方法方法都觸發了,看來是把 view 徹底重新走了一遍,大家想想也是,view 的位置和大小要是變化了呢。有的文章說 requestLayout 不會觸發 onMeasure 方法,但是自己跑一下,還不是發現3個方法都執行了,也許是 android 版本的問題,這里我使用的是 API 26

  • requestFocus
    這個方法我試了下,不管是游參數的,沒參數的都不會觸發任何函數,這個方法我也不熟,姑且簡單的直接執行了下,沒觸發任何 view 的函數我也是有些奇怪。就這樣吧,哪位看到這里有了解的,請在評論里指點一下

2. view 位移

我們就來簡單的設置下 translationY 來看看

結果是沒有觸發任何 view 的方法,簡單翻翻 setTranslationY 方法的源碼,里面調用的也是 invalidate 方法,但是沒看到 view 的3個核心方法被觸發,這么說 view 的位移并不是我們簡單想想的 重新布局,具體啥怎么執行的,我也不知道啊,大家自己去研究把

3. view 位置變化

上面我們操作的是 view 的以為屬性參數來移動的 view ,那么我們來試試 setTop 這類直接改變 view 坐標參數的方法


Snip20180628_7.png

view 的大小本質是由 left,top,buttom,right 4個參數決定的,所以我們改了這4個參數的任意一個就是改變了 view 的大小,可以看到 觸發了 view 的 onSizeChange 大小改變函數,然后重新繪制,布局方法沒有觸發我有點想不通,大小改變了其實也算是 view 的位置改變了啊。


視圖層級和多次測量,布局的關系

view 的計算,布局,繪制函數不是自己調用的,而是父控件 ViewGroup 決定何時,何地調用子 view 的相關方法

這里涉及到一個結論:

ViewGroup 繪制一次會調用子 view 的2次 onMeasure ,2次 onLayout ,1次 onDraw,部分 ViewGroup 具體子類會對自身測量2次,那么就會 調用子 view 的4次 onMeasure

官方文檔有句話:

ViewGroup 會執行2 測量,第一次使用 USPENCIFIED 模式測量子 view 的真實大小,第二次使用 EXACTLY 模式再次測量子view

所以就產生了 2次 onMeasure ,2次 onLayout ,1次 onDraw。我打印了一下 view 2次測量,發現其實2次測量傳入的測量模式參數都是一樣的,具體原因有人說是 viewRootImpl 的原因,viewRootImpl 會執行2測測量,詳細的自己去 Google 下

然后我測試了下,發現不同的 ViewGroup 子類中,根據子 view 的測量模式不同,會對子 view 的測量方法執行次數造成影響

FrameLayout 幀布局

view 不論用的哪種測量模式都會造成子 view 2次測量

ConstraintLayout 約束布局

view 使用 match_parent 會執行4測測量,使用具體的寬高值時,比如200dp,只會執行2次測量

match_parent


Snip20180629_9.png

200dp * 300dp


Snip20180629_10.png

從上面的打印信息中,可以清晰的看到 ConstraintLayout 約束布局中 match_parent 測量模式會測量出前后不同的高度參數。

RelativeLayout 相對布局

不管是 match_parent 還是 200dp ,都會觸發自子 view 4次測量

match_parent


Snip20180629_16.png

200dp * 400dp


Snip20180629_14.png
LinearLayout 線性布局

同 FrameLayout 幀布局,只會2次測量

關于執行4次 view 的測量可以這樣解釋

某些 ViewGroup 子類會對 ViewGroup 自身執行2次測量,進而引起子 view 4次測量

在上面的打印日志中,可以看到 viewRootImpl 的身影,viewRootImpl 在第二次測量之后觸發了一個大小改變的函數,然后接著是2次測量。可以猜測下是不同的 ViewGroup 子類有時候會觸發 window 視圖的變化,從而連帶著造成 ViewGroup 的再次測量,ViewGroup 一次測量會對子 view 測量2次,那么 window 2次測量 ViewGroup 就會對子 view 測量4次


在多視圖層級下,最內層的 view 執行的測量次數和父控件層數和類型的關系

那么繼續深入,在多視圖層級下,最內層的 view 會執行幾次測量呢。

來個3層視圖層級的例子,父視圖都是 ConstraintLayout,view 是 match_parent 測量模式

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.bloodcrown.aaa02.MeasureActivity">

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.bloodcrown.aaa02.MyView2
                android:id="@+id/view_my"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

        </android.support.constraint.ConstraintLayout>
    </android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>

結果:


Snip20180629_17.png

view 執行了16 次測量,16次 = 內層 4次 * 第二層視圖 2次 * 第一層視圖 2次,我這里用的都是 match_parent,外層 ViewGroup 都會對自己執行2遍測量,比如視圖第二層的 ViewGroup ,對自己測量一遍就是最內層的 ViewGroup 對 view 測量4遍,那么 視圖第二層的 ViewGroup 對自己測量2遍,就是 最內層的 ViewGroup 對 view 測量8遍,以此類推,就是 16遍

我用 systrace 工具專門看了下這一幀的執行情況


Snip20180629_22.png

systrace 工具自動分析已經開始提示昂貴的測量,布局損耗了,僅僅只是 16 次測量而已,就花了 6 毫秒,大家看到沒 view 的 測量很耗費 cpu 資源的,所以我們應該盡可能的減少 view 的測量,優化布局性能任重而道遠

我們把第二層視圖 ViewGroup 改成 200dp * 200dp ,猜測下 view 應該會執行 4次 * 1次 * 2次,一共 8次測量

看結果:的確是 8次


Snip20180629_20.png

那我們把多有的上層 ViewGroup 都改成 200dp * 200dp

看結果:


Snip20180629_18.png

view 執行2次,等于 ViewGroup 只執行了一次測量,當然在實際中 ViewGroup 不可能都能有個具體的寬高值,但是在我們能夠明確知道布局的寬高時,寫具體數據是能夠大大減少布局中 view 的測量次數的

這個話題就寫到這里了,還有其他想法的同學自己試著玩吧。另外我們有興趣的同學可以用 systrace 全程觀察下


最后我們來看下 view 整個 acitivty 頁面中的方法調用

20161030164616308.png

在 acitivty resume 之后 布局view 才會加入到 window 中,然后開始整個布局的測量,布局,繪制

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容