Android 開發藝術探索 - 讀書筆記之第四章 View 的工作原理

4.1 初識 ViewRoot 和 DecorView

  • ViewRootImpl
    注意 ViewRootImpl 并不是一個 View,但它實現了 ViewParent 接口,WindowManager 通過它來指揮 DecorView 的運作;View 的 measure/layout/draw 三大流程都是在 ViewRoot 的 performTraversals 方法中依次調用 performMeausre/performLayout/performDraw 方法,并遍歷 View 樹的。

  • DecorView
    是整個視圖樹的根節點;是 Window 的實現 PhoneWindow 類的私有內部類,是一個 FrameLayout,默認包含標題欄和內容欄,內容欄是一個 id 為“andorid.R.id.content”的 FrameLayout,我們調用 Activity 的 setContentView,就是向這個內容欄中添加視圖。

4.2 理解 MeasureSpec

“測量規格”或“測量說明”,見 View 下的 MeasureSpec 公共靜態內部類

4.2.1 MeasureSpec

先看一個最普通的 View 的布局定義:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" />

“layout_”開頭的屬性是跟測量有關系的屬性,它可以是“wrap_content”,可以是“match_parent”,也可以指定精確的值例如“18dp”。
在代碼中,這個屬性封裝在 LayoutParams 中,并最終轉化為一個 MeasureSpec,是一個 32 位 int 值,高2位代表 SpecMode,低30位代表 SpecSize。
SpecMode 有三類:

  • UNSPECIFIED,要多大給多大,出現在系統內部,一般不常見
  • EXACTLY,精確值,對應布局定義中的 “match_parent” 和具體數值的情況。
  • AT_MOST,最大不能超過一個大小,對應布局中的“wrap_content”

4.2.2 MeasureSpec 和 LayoutParams 的對應關系

當我們在指定一個 View 的布局屬性(LayoutParams)時,最終的測量結果有可能有所不同,這是因為測量結果會受到父容器的約束。當為 View 指定了 LayoutParams 后,交給父容器計算后得到的 MeasureSpec 才是真正的測量結果。例如指定“match_parent”,經過父容器計算返回“100px”,View 就可以拿這個結果確定大小,并進行繪制了:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在 getChildMeasureSpec 方法中就體現了子視圖的 MeasureSpec 的創建規則,跟父容器的 MeasureSpec 與子 View 的 LayoutParams 都有關系。
例如,若父容器是“match_parent”,子視圖也是“match_parent”,那子視圖的 SpecMode 就是 “EXACTLY”,SpecSize 就是父容器的大小去掉 padding;若父容器是“wrap_content”,子視圖是“10px”,那子視圖的 SpecMode 就是 “EXACTLY”,SpecSize 就是“10”;等等,不一而足,具體請看源碼,這部分邏輯還蠻簡單的。

當然作為頂級 View 的 DecorView 來說,它沒有父容器了,它的 MeasureSpec 自然也只受窗口(Window)和自身的 LayoutParams 決定了。在 ViewRootImpl 的 measureHierarchy 方法的源碼中有所體現,這里就不贅述。

4.3 View 的工作流程

在 ViewRootImpl 的 performMeausre 方法中,從根節點開始執行深度優先遍歷,依次調用每一個節點的 measure 方法。每個 ViewGroup 通過遍歷調用所有的非 GONE 的子視圖的 measure 方法來測量自身。

4.3.1 measure 過程

measure 方法是一個 final 方法,它把測量的具體實現交給了 onMeasure 方法。View 類中提供了該方法的默認實現。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

從這個 getDefaultSize 方法中可以看出,AT_MOST 和 EXACTLY 被一視同仁,后果是使用“wrap_content”就相當于“match_parent”。

所以要自定義 View 支持 “wrap_content” 的話,就必須要重寫 onMeasure 方法了。

對于一個 ViewGroup 而言,它的測量工作不僅取決于子視圖的測量結果,也與它自身的布局特性有關,所以自定義布局也必須重寫 onMeasure 方法。

由于 View 的 Measure 過程與 Activity 的生命周期方法不是同步執行的, 所以在 Activity 生命周期方法中無法獲取一個 View 的寬高信息,關于這個問題,書中給出四種方法:

  1. Activity/View#onWindowFocusChanged
  2. view.post(runnable)
  3. ViewTreeObserver
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
});
  1. view.measure(a, b)
    這種方法要根據 view 的布局屬性來分情況討論:
  • match_parent
    無法計算,直接放棄此方法

  • wrap_content
    使用理論最大值構造 MeasureSpec

int widthMeasureSpect = 
        MeasureSpec.makeMeasureSpec((1 << 30), View.MeasureSpec.AT_MOST);
int heightMeasureSpect = 
        MeasureSpec.makeMeasureSpec((1 << 30), View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpect, heightMeasureSpect);
  • 具體的值
int widthMeasureSpect = 
        MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpect = 
        MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpect, heightMeasureSpect);

4.3.2 layout 過程

Layout 的作用就是確定視圖的位置,具體就是通過 layout 方法,確定自身相對父容器的位置以及大小的信息。
對于 ViewGroup 而言,就是通過 onLayout 進行一定的計算遍歷調用所有子視圖的 layout 方法。

layout 的基本流程就是:

  1. setFrame 設定 view 的四個位置屬性(mLeft,mTop, mRight, mBottom)
  2. 調用 onLayout 確定子元素位置(View 和 ViewGroup 均沒有實現 onLayout)

通過觀察 getWidth() 的實現:

public final int getWidth() {
    return mRight - mLeft;
}

可以看出 getWidth() 與 getMeasuredWidth() 的區別,前者是在 layout 過程中形成的,后者是在 measure 過程中形成的。

4.3.3 draw 過程

draw 過程可以說是三大流程中最簡單的,繪制過程遵循以下幾步:

  1. 繪制背景(background.draw(canvas))
  2. 繪制自身(onDraw)
  3. 繪制子視圖(dispatchDraw)
  4. 繪制裝飾(onDrawScrollBars)

4.4 自定義 View

4.4.1 自定義 View 的分類

自定義 View 分為:

  • 自定義繪制,例如繪制鐘表,貝塞爾曲線之類的
  • 自定義布局,例如瀑布流布局,標簽流布局

4.4.2 自定義 View 須知

  • 支持“wrap_content”
  • 支持“padding”
  • 在 onDetachedFromWindow 中終止動畫和線程
  • 處理滑動沖突

4.4.3 自定義 View 示例

示例太多了,在了解了原理之后,多瀏覽 github,技術博客別人分享的源碼吧

推薦閱讀

Hongyang
郭霖 - Android自定義View的實現方法
stormzhang
View進行自定義UI

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

推薦閱讀更多精彩內容