View 的工作原理
理解 View 的工作原理,熟悉自定義 View 的各種套路。
1. ViewRoot 和 DecorView
- ViewRoot 對應於 ViewRootImpl 類,它是連線 WindowManager 和DecorView 的紐帶。
- View 的繪製流程是從 ViewRoot 的
performtraversals
方法開始的:
- measure 的過程決定了 View 的寬高,完成後可以通過 getMeasuredWidth 和 getMeasuredHeight 獲取。
- layout 的過程決定了 View 的四個頂點的座標和實際寬高,完成後可以通過
getTop
、getBottom
、getLeft
和getRight
獲取。 - draw 的過程決定了 View 的顯示,完成後才能呈現在螢幕上。
- 在 Activity 中通過
setContentView
的佈局可以通過以下方式獲取到:ViewGroup content = findViewById(android.R.id.content);
2. MeasureSpec
- MeasureSpec 和父容器決定了 View 的尺寸規格。
-
MeasureSpec 是一個 32 位的 int 值,高 2 位代表 SpecMode ,低 30 位代表 SpecSize 。
> int mode = MeasureSpec.getMode(spec); > int size = MeasureSpec.getSize(spec); >
-
SpecMode 有三類:
- UNSPECIFIED :父容器不對 View 有任何限制,要多大給多大。
- EXACTLY :父容器已經檢測出 View 所需的精確代銷,這時 View 的最終大小就是 SpecSize 的值,它對應於 LayoutParams 中的 match_parent 或具體大小數值。
- AT_MOST :父容器指定了一個大小 SpecSize,View 不能大於這個值,最終大小取決於 View 的具體實現,它對應於 LayoutParams 中的 wrap_content。
- 我們在給 View 設定 LayoutParams 後,系統會在父容器的約束下將 LayoutParams 轉換成對應的 MeasureSpec。
- 頂級 DecorView,其 MeasureSpec 由視窗尺寸和自身 LayoutParams 決定。
- 普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身 LayoutParams 決定。
- 普通 View 的 MeasureSpec 建立規則:
childLayoutParams \ parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
match_parent | EXACTLY parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
3. View 的工作流程
3.1 measure 過程
- 分兩種情況,如果是一個原始 View,通過
measure
方法就完成了測量過程;如果是 ViewGroup,除了完成自己的測量,還要遍歷子元素的measure
方法。 - 直接繼承 View 的自定義控制元件需要重寫
onMeasure
方法並設定 wrap_content 時的自身大小,否則在佈局中使用 wrap_content 時就相當於使用 match_parent; - ViewGroup 是一個抽象類,並沒有定義其具體的測量過程,但它提供了一個
measureChildren(int widthMeasureSpec, int heightMeasureSpec)
的方法來測量子元素,該方法裡迴圈呼叫measureChild(child, widthMeasureSpec, heightMeasureSpec)
方法,measureChild
的思想就是取出元素的 LayoutParams,然後通過getChildMeasureSpec
方法建立子元素的 MeasureSpec,最後將 MeasureSpec 傳遞給 View 的measure
方法來進行測量。 - 簡化版的流程圖如下:
- 由於在一些極端情況下,系統要在多次 measure 後才能確定測量的結果,所以在 onMeasure 中拿到的寬高可能不準確,推薦在 onLayout 中獲取。
-
準確獲取 View 寬高的四種方式:
- Activity/View#onWindowFocusChanged
- view.post(runnable)
-
ViewTreeObserver
... ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener ...
-
view.measure(int widthMeasureSpec, int heightMeasureSpec)
3.2 layout 過程
- View 的 onLayout 方法為空實現,而 ViewGroup 繼承自 View 也沒有實現該方法,所以 onLayout 具體實現由父容器的特性決定,比如 LinearLayout。
- 簡易的流程圖如下:
- 在 View 的預設實現中,View 的測量寬高和最終寬高是相等的,只不過測量寬高形成於 measure 過程,而最終寬高形成於 layout 過程,兩者賦值時機不同。
3.3 draw 過程
- draw 的繪製過程遵循如下幾步:
background.draw(canvas) onDraw dispatchDraw onDrawScrollBars
- ViewGroup 預設啟用繪製優化標記位,如果 ViewGroup 需要通過 onDraw 來繪製內容,我們可以通過
setWillNotDraw(false);
來關閉該標記位。
4. 自定義 View
- 自定義 View 大體可以分為四類:
- 繼承 View 重寫 onDraw 方法
- 繼承 ViewGroup 派生特殊的 Layout
- 繼承特定的 View(比如 TextView)
- 繼承特定的 ViewGroup(比如 LinearLayout)
- 自定義 View 須知:
post View#onDetachedFromWindow
本章節配套原始碼 ofollow,noindex">戳我