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

Android 開發中常常須要用一些自定義 View 去知足產品和設計的腦洞,因此 View 的繪製流程相當重要。網上目前有很是多這方面的資料,但最好的方式仍是直接跟着源碼進行解讀,每日一問系列一直追求短平快,因此本文筆者儘可能精簡。java

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

基本繪製都是會從根視圖 ViewRootperformTraversals() 方法開始,從上到下遍歷整個視圖樹,每一個View控件負責繪製本身,而 ViewGroup 還須要負責通知本身的子 View 進行繪製操做。performTraversals() 的核心代碼以下:github

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 是同樣的顯示效果。至於緣由,其實一探源碼便知。json

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() 的返回值。canvas

getSuggestedMinimumWidth() 會直接根據是否設置 backgroud 來進行計算,須要注意的是,直接設置 color 做爲 backgroud 也會直接採用 minXXX 的值。佈局

ViewGroup 中,並無去重寫 ViewonMeasure() 方法,而這都須要它的子類根據本身的邏輯去實現,好比 LinearLayoutRelativeLayout 明顯測量邏輯是不同的。不過,ViewGroup 卻是提供了一個 measureChildren() 方法來依次遍歷每一個子 View 對其進行測量。post

在通過 onMeasure() 操做後,getMeasureWidth()getMeasureHeight() 方法就能夠拿到正確的返回值了。spa

因爲 View 的 measure 過程和 Activity 的生命週期方法不是同步執行的,若是 View 尚未測量完畢,那麼得到的寬/高就是 0。因此在 onCreate()onStart()onResume() 中均沒法正確獲得某個 View 的寬高信息。能夠經過在 onWindowFocusChanged() 判斷獲取到焦點後進行獲取,或者使用 view.post() 方式。.net

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

相關文章
相關標籤/搜索