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 的寬高信息,關于這個問題,書中給出四種方法:
- Activity/View#onWindowFocusChanged
- view.post(runnable)
- 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();
}
});
- 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 的基本流程就是:
- setFrame 設定 view 的四個位置屬性(mLeft,mTop, mRight, mBottom)
- 調用 onLayout 確定子元素位置(View 和 ViewGroup 均沒有實現 onLayout)
通過觀察 getWidth() 的實現:
public final int getWidth() {
return mRight - mLeft;
}
可以看出 getWidth() 與 getMeasuredWidth() 的區別,前者是在 layout 過程中形成的,后者是在 measure 過程中形成的。
4.3.3 draw 過程
draw 過程可以說是三大流程中最簡單的,繪制過程遵循以下幾步:
- 繪制背景(background.draw(canvas))
- 繪制自身(onDraw)
- 繪制子視圖(dispatchDraw)
- 繪制裝飾(onDrawScrollBars)
4.4 自定義 View
4.4.1 自定義 View 的分類
自定義 View 分為:
- 自定義繪制,例如繪制鐘表,貝塞爾曲線之類的
- 自定義布局,例如瀑布流布局,標簽流布局
4.4.2 自定義 View 須知
- 支持“wrap_content”
- 支持“padding”
- 在 onDetachedFromWindow 中終止動畫和線程
- 處理滑動沖突
4.4.3 自定義 View 示例
示例太多了,在了解了原理之后,多瀏覽 github,技術博客別人分享的源碼吧