測量流程

稀飯_發表於2018-07-19

通過上一文章分析我們找到了ViewRootImpl類中的performTraversals方法是測量佈局和繪製的起點。並且由Activity中的setContentView觸發。這一篇我們將從performTraversals方法分析View的測量過程。

  private void performTraversals() {
        ...
        //測量
        if (!mStopped) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
    //佈局
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
    }


    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            //繪製
            performDraw();
        }
    }
    ...
}複製程式碼

每個View的MeasureSpec都是由父容器的MeasureSpec+自己的LayoutParams轉換成自己的MeasureSpec。轉換的原始碼在View基礎看過。這裡我們主要看對於DecorView來說,它已經是頂層view了,沒有父容器,那麼它的MeasureSpec怎麼來的?

回到原始碼我們看通過getRootMeasureSpec方法獲取的

/**
 * @param windowSize 螢幕的寬或者高
 *
 * @param rootDimension 下邊展開這個引數
 *
 * @return 根檢視的度量規範
 */
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            //這種模式就是撐滿布局,大小是佈局大小windowSize。模式的EXACTLY
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            //包裹內容,最大大小勢能超過windowSize,模式是AT_MOST
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 確定的值,那麼就是吧確定的rootDimension給他
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}複製程式碼

第二個引數是中的值是獲取的這個物件的寬度和高度

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();複製程式碼

他在預設情況下都是match_parment,也就意味著根檢視能給子類全部的螢幕大小區域。

到目前為止,就已經獲得了一份DecorView的MeasureSpec,然後呼叫performMeasure方法

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}複製程式碼

這個方法很簡單,直接呼叫mView的measure方法,並且傳入根佈局的度量規範,那麼這個mView是誰呢?

通過檢視原始碼可以知道這個mView就是DecorView

這是ViewRootImpl#setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;複製程式碼

到此:

  • 我們得到了DecorView的MeasureSpec
  • 呼叫了DecorView中的measure方法並傳入DecorView的MeasureSpec

現在我們去找DecorView中的measure方法

由於DecorView繼承自FrameLayout,而FrameLayout和DecorView和ViewGroup中沒有measure方法(final表示不能重寫,所以它的子類不可能有measure方法),因此呼叫的是父類View的measure方法,我們直接看它的原始碼

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    onMeasure(widthMeasureSpec, heightMeasureSpec);
}複製程式碼
View#measure中又呼叫了onMeasure(widthMeasureSpec, heightMeasureSpec)方法。並且DecorView重寫了onMeasure方法,在DecorView#onMeasure方法中主要是
進一步確定自己的widthMeasureSpecheightMeasureSpec,並呼叫super.onMeasure(widthMeasureSpec, heightMeasureSpec)FrameLayout#onMeasure方法

由於不同的ViewGroup有著不同的性質,那麼它們的onMeasure必然是不同的,因此這裡不可能把所有佈局方式的onMeasure方法都分析一遍,因此這裡選擇了FrameLayout的onMeasure方法來進行分析,其它的佈局方式讀者可以自行分析。那麼我們繼續來看看這個方法:

注意:這裡沒有呼叫super.onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //獲取當前佈局內的子View數量
    int count = getChildCount();
    ////遍歷所有型別不為GONE的子View,
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 這個方法的作用:結合layoutparmas(使用者引數)生成子類的測量規範
            //呼叫測量子類的方法,傳入上邊生成的測量規範
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
    }

    //這兩個方法都是View中的方法,
    //設定DecorView的大小 這裡設定的是螢幕的寬高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

}
複製程式碼
protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {
    //獲取子類的佈局引數
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
  
    //使用者和父佈局共同商量的結果
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    //呼叫測量子類的方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}複製程式碼


child物件呼叫的measure是View中的measure,而View中的measure又呼叫線性佈局中的onMeasure方法,那麼我們去找線性佈局中的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}複製程式碼

這裡我們要畫一張圖片來表示DecorView的佈局

測量流程

所以我們需要繼續去看measureVertical的原始碼裡邊的關鍵程式碼

父容器的MeasureSpec+自己的LayoutParams轉換成自己的MeasureSpec

final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
        mPaddingLeft + mPaddingRight +
                lp.leftMargin + lp.rightMargin, lp.width);複製程式碼

繼續呼叫子類的測量

child.measure(childWidthMeasureSpec,
        MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));複製程式碼

同時設定自己大小

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);複製程式碼

當子View是基本View的時候,他不會再去呼叫子類的測量方法,僅僅去獲取父容器的MeasureSpec+自己的LayoutParams轉換成自己的MeasureSpec,然後只需要設定自己的大小即可。

到此關於VIew的測量分析完畢。

梳理一下測量流程:

1.Activity中的setContentView方法觸發ViewRootImpl中的performTraversals方法執行

2.performTraversals方法觸發performMeasure方法執行(注意這裡的MeasureSpec生成方法是make建立的

3.DecorView物件呼叫View#measure方法執行

4.View#measure方法呼叫不同的onMeasure方法執行(因為子類重寫了)

5.在繼承ViewGroup的onMeasure方法中會做兩件事

  • 獲取父容器的MeasureSpec+子ViewLayoutParams轉換成自己的MeasureSpec然後呼叫子View的測量方法(measureChildWithMargins方法內邏輯即使)
  • 通過setMeasuredDimension方法設定自己的大小

6.在繼承View的onMeasure方法中只會做一件事,參考父容器的MeasureSpec,然後通過setMeasuredDimension方法設定自己的大小


分析原始碼需要注意:

  • 呼叫measure方法是同一個方法
  • 呼叫onMeasure確是不同的onMeasure方法。

而且可以發現:測量的時候是先去測量子View的大小,之後再去測量自己的大小



相關文章