四、View 繪製流程 —— 隨想(2)
如需詳細瞭解,請看 [參考] 連結。
1. measure
measure 用於測量 view 的寬 / 高
MeasureSpec
模式 | 具體描述 | 應用場景 | 備註 | 思考 |
---|---|---|---|---|
UNSPECIFIED | ||||
EXACTLY | 子檢視代下必須在父檢視指定的確切尺寸內 | match_parent 或 具體數值(如100dp) | 當為具體數值時,View的最終大小就是Spec指定的值,所以父控制元件可通過 MeasureSpec.getSize()直接得到子控制元件的尺寸 | |
AT_MOST | 父檢視為子檢視指定一個最大尺寸,子檢視必須確保自身和它是所有子檢視可適應在該尺寸內 | 自適應大小(wrap_content) | 該模式下,父控制元件無法確定子View的尺寸,只能由子控制元件自身根據需求計算尺寸。該模式= 自定義檢視需要實現測量邏輯的情況 | 也就是說該模式下,自定義View需要自行實現onMeasure方法,確保測量準確?是的,給出預設大小 |
注:圖可能會不準確,因為是根據自己的思維走的流程,所以會省略很多已知東西。

measure.png
1.1 問題1
分析完後,知道了 MeasureSpec 的作用,以及 ViewGroup 中的 getChildMeasureSpec 方法。在分析這個方法的時候,知道了如果子 View 沒有給出具體的 dp 大小,那麼測量出的大小會等於父容器當前剩餘空間的大小。
即int size = Math.max(0, specSize - padding);
在看的時候沒有任何問題,但是自己想著想著的時候,突然被繞進去了,想到一個問題
Q1: 當父ViewGroup 為 match_parent,子 View 是 wrap_content 時,子 View 的大小應該是多少 ?
A1:
因為根據 getChildMeasureSpec 方法可以知道,這個時候子 View 的大小是等於 父容器剩餘空間的大小的,可是當我們用 ImageView,TextView 等做例子時,會發現他們並不是填充整個父容器,而是有著 剛好適應內容的最小尺寸 的。這個我就暈了,為啥跟結論不對呢。這個疑惑一直困擾著我看原始碼。 後面才發現,ImageView、TextView 他們是複寫了 onMeasure 的,在裡面針對 wrap_content 的情況,會給寬/高一個預設值,當然這個預設值是有特殊處理的,至於怎麼處理,檢視他們的原始碼即可。
到這裡終於解決了這個疑惑,原來是通過指定一個預設大小 (寬 / 高) 解決的這個問題。
總結: 直接繼承 View 的自定義控制元件需要重寫 onMeasure 方法並設定 wrap_content 時的自身預設大小,否則在佈局中使用 wrap_content 就相當於使用 match_parent。
TextView onMeasure 部分原始碼
// Check against our minimum width // width 在上面還會做各種處理,為了找到最小的 width width = Math.max(width, getSuggestedMinimumWidth()); //當widthMode 為 AT_MOST,即 wrap_content 時,給 width 設定預設大小 if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(widthSize, width); }
1.2 問題2
我們現在知道在 measure 的時候,父 View 會傳入自己的 MeasureSpec 給子 View,用於測量。
即 public final void measure(int widthMeasureSpec, int heightMeasureSpec)
中的 widthMeasureSpec、heightMeasureSpec 引數。
Q2:那麼假設 LinearLayout佈局,orientation 為 vertical 的 ViewGroup 測量時,他並不知道自己的heightMeasureSpec 的 SpecSize 是多大呀,那子 View 是怎樣在 getChildMeasureSpec() 中得到 parentSize 的呢。
A1:根據原始碼弄清楚流程。
// Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). final int usedHeight = totalWeight == 0 ? mTotalLength : 0; measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
這段是 LinearLayout 中 measureVertical 方法裡 對子 View 進行 for 迴圈的那段。 這裡對每個子 View 進行 measure 測量,而 measureChildBeforeLayout 裡的 heightMeasureSpec 引數就是問題的疑惑點。
在 Q2 的中透露出來這麼一個邏輯導致了這樣一個問題的產生。那就是在測量時,我們都知道 measureSpec 測量規格是由 父View 傳來的,而在第一印象中 LinearLayout 就是子 View 的父View,所以我就會想這裡既然這裡是對子 View 進行測量,那這個 heightMeasureSpec 肯定是 LinearLayout 的 heightMeasureSpec 嘛,可是在垂直佈局的 LinearLayout 中,高度是還不確定的,因為子 View 還沒測量完,所以這裡產生了疑惑。
後面仔細看程式碼,發現自己繞進去了,原來這裡的 heightMeasureSpec 是 LinearLayout 的父 View 的heightMeasureSpec。因此解除了心中的疑惑,
2. layout

layout.png
因為 onLayout 在 ViewGroup中是抽象方法,所以自定義 ViewGroup,必須重寫該方法。下面的程式碼為 自定義ViewGroup 中的 onLayout 偽碼實現,這是通常的做法,具體怎麼實現取決你自己
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //1. 遍歷子View:迴圈所有子View for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); //2. 計算當前子View的四個位置值 //2.1 位置的計算邏輯 //需自己實現,也是自定義View的關鍵 //2.2 對計算後的位置值進行賦值 int mLeft = left; int mTop = top; int mRight = right; int mBottom = bottom; //3. 根據上述4個位置的計算值,設定View的4個頂點,呼叫子View的layout child.layout(mLeft, mTop, mRight, mBottom); } }
3. draw

draw.png
問題 : onDraw 只有在 View 裡生效,ViewGroup 重寫了也無用。( View 的特殊方法 setWillNotDraw )
學習了 draw 流程後立馬嘗試了下,卻發現在 ViewGroup 中不生效,此時我的心是撥涼撥涼的,後面查閱資料發現是由於一個小細節。
View.setWillNotDraw()
這個方法在搗蛋。
- 這是 View 中的特殊方法,它的作用是:當一個 View 不需要進行繪製時,系統會進行相應優化。
- 設為 false 代表不啟動該標誌位,即 需要進行繪製 ;
- 設為 true 代表啟動該標誌位,即 不需要進行繪製 。
- 在預設情況下:View 是設為 false, 而 ViewGroup 是設為 true 的,所以導致了ViewGroup 沒生效。
- 應用場景
a. setWillNotDraw引數設定為true:當自定義View繼承自 ViewGroup 、且本身並不具備任何繪製時,設定為 true 後,系統會進行相應的優化。
b. setWillNotDraw引數設定為false:當自定義View繼承自 ViewGroup 、且需要繪製內容時,那麼設定為 false,來關閉 WILL_NOT_DRAW 這個標記位。
4. 在 Activity 中正確的獲取某個 View 的 寬 / 高
-
4.1 在 Activity 中的 onWindowFocusChanged 方法獲取
-
4.2 View.post(Runnable)
-
4.3 ViewTreeObserver 獲取
上述三種方法的獲取程式碼如下:
private CustomView customView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); customView = findViewById(R.id.custom_view); //嘗試在onCreate 中獲取 Log.d(TAG, "onCreate: ----------->"+customView.getMeasuredWidth()); Log.d(TAG, "onCreate: getWidth----------->"+customView.getWidth()); //4.2 post 方法 customView.post(new Runnable() { @Override public void run() { Log.d(TAG, "post Runnable: ----------->"+customView.getMeasuredWidth()); } }); //4.3 ViewTreeObserver 方法 ViewTreeObserver observer = customView.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Log.d(TAG, "ViewTreeObserver: ----------->"+customView.getMeasuredWidth()); } }); } //4。1 onWindowFocusChanged 方法 @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Log.d(TAG, "onWindowFocusChanged: ----------->"+customView.getMeasuredWidth()); }
執行結果如下

viewtreeobserver.png
5. 參考
ofollow,noindex">https://www.jianshu.com/p/146e5cec4863
https://blog.csdn.net/yanbober/article/details/46128379