每日一問:簡述 View 的繪製流程

南塵發表於2019-06-11

Android 開發中經常需要用一些自定義 View 去滿足產品和設計的腦洞,所以 View 的繪製流程至關重要。網上目前有非常多這方面的資料,但最好的方式還是直接跟著原始碼進行解讀,每日一問系列一直追求短平快,所以本文筆者儘量精簡。

想必大多數 Android 開發都知道自定義 View 需要關注的幾個方法:onMeasure()onLayout()onDraw(),這其實也是每個 View 至關重要的繪製流程。

基本繪製都是會從根檢視 ViewRootperformTraversals() 方法開始,從上到下遍歷整個檢視樹,每個View控制元件負責繪製自己,而 ViewGroup 還需要負責通知自己的子 View 進行繪製操作。performTraversals() 的核心程式碼如下:

private void performTraversals() {
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //執行測量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //執行佈局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //執行繪製流程
    performDraw();
}

measure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

每個 View 都有自己的大小,所以基本自定義 View 的時候都需要重寫 onMeasure() 這個方法,以定製化我們的 View 的寬高。如果不重寫這個方法,我們通常會出現 wrap_contentmatch_parent 是一樣的顯示效果。至於原因,其實一探原始碼便知。

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;
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}       

可以看到,View 預設是會使用 getDefaultSize() 方法進行設定寬高的,在 AT_MOSTEXACTLY 兩種情況下都會直接使用測量規格里面的尺寸。在 UNSPECIFIED 模式下會直接取getSuggestedMinimumWidth() 的返回值。

getSuggestedMinimumWidth() 會直接根據是否設定 backgroud 來進行計算,需要注意的是,直接設定 color 作為 backgroud 也會直接採用 minXXX 的值。

ViewGroup 中,並沒有去重寫 ViewonMeasure() 方法,而這都需要它的子類根據自己的邏輯去實現,比如 LinearLayoutRelativeLayout 明顯測量邏輯是不一樣的。不過,ViewGroup 倒是提供了一個 measureChildren() 方法來依次遍歷每個子 View 對其進行測量。

在經過 onMeasure() 操作後,getMeasureWidth()getMeasureHeight() 方法就可以拿到正確的返回值了。

由於 View 的 measure 過程和 Activity 的生命週期方法不是同步執行的,如果 View 還沒有測量完畢,那麼獲得的寬/高就是 0。所以在 onCreate()onStart()onResume() 中均無法正確得到某個 View 的寬高資訊。可以通過在 onWindowFocusChanged() 判斷獲取到焦點後進行獲取,或者使用 view.post() 方式。

layout()

public void layout(int l, int t, int r, int b)

我們可以重寫的 onLayout() 方法主要作用是確定子 View 的顯示位置,由於 View 已經是最小的層級,所以我們在自定義 View 的時候通常不需要管這個方法,而在自定義 ViewGroup 的時候就不得不注意這個方法了。

經過 onLayout() 流程後,我們的 leftrighttopbottom 得以賦值,所以這時候可以通過 getWidth()getHeight() 方法來獲取 View 的實際寬高了。

注意:在 View 的預設實現中,View 的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成於 View 的 measure 過程,而最終寬/高形成於 View 的 layout 過程,即兩者的賦值時機不同,測量寬/高的賦值時機稍微早一些。在一些特殊的情況下則兩者不相等:

draw()

public void draw(Canvas canvas)

繪製的流程也就是通過呼叫 View 的 draw() 方法實現的。draw() 方法裡的邏輯看起來更清晰,我就不貼原始碼了。一般是遵循下面幾個步驟:

  • 繪製背景 – drawBackground()
  • 繪製自己 – onDraw()
  • 繪製孩子 – dispatchDraw()
  • 繪製裝飾 – onDrawScrollbars()

由於不同的控制元件都有自己不同的繪製實現,所以V iew 的 onDraw() 方法肯定是空方法。而 ViewGroup 由於需要照顧子 View 的繪製,所以肯定在 dispatchDraw() 方法裡遍歷呼叫了child的 draw() 方法。

參考:
Android View的繪製流程
https://blog.csdn.net/yisizhu/article/details/51527557

相關文章