wrap_content/match_parent為何可以靈活控制視圖的寬高

視圖的寬高可以靈活變化

android對(duì)于的視圖布局的定義提供了一種很靈活的實(shí)現(xiàn),就是當(dāng)給視圖的寬高屬性設(shè)置wrap_content/match_parent,視圖可以根據(jù)所處布局的情況動(dòng)態(tài)的改變寬高。

wrap_content:根據(jù)自視圖的內(nèi)容決定視圖大小
match_parent:占滿父視圖空間

wrap_content/match_parent有什么奧秘能夠使視圖的寬高靈活變化呢?

measureSpec影響寬高的計(jì)算

一個(gè)視圖的繪制過(guò)程主要有三個(gè)部分:測(cè)量(measure),布局(layout)、繪制(draw)。其中測(cè)量過(guò)程就是計(jì)算一個(gè)視圖寬高的過(guò)程,主要通過(guò)onMeasure方法實(shí)現(xiàn)。

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 ...
}

onMeasure方法接收來(lái)至父視圖對(duì)子視圖寬高的約束measureSpec,具體的視圖會(huì)根據(jù)measureSpec來(lái)計(jì)算視圖的寬高。
measureSpec的一些概念:
measureSpec是一個(gè)32位的值,其前兩位表示測(cè)量的模式,后30位表示測(cè)量的大小。
測(cè)量模式有三種:
EXACTLY:父控件給子控件決定特定的大小。
AT_MOST:子控件至多達(dá)到指定大小的值。
UNSPECIFIED:父控件沒有對(duì)子控件施加任何約束,子控件可以得到任意想要的大小。
android提供了相應(yīng)的方法來(lái)處理measureSpec

// 獲取測(cè)量模式
int specMode = MeasureSpec.getMode(measureSpec);
// 獲取測(cè)量大小
int specSize = MeasureSpec.getSize(measureSpec);
// 通過(guò)大小和模式生成measureSpec
MeasureSpec.makeMeasureSpec(resultSize, resultMode);

例如 TextView的onMeasure實(shí)現(xiàn):

// onMeasure的關(guān)鍵實(shí)現(xiàn)
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
           ...           
            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);
            }
        }
        ...
        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {
            ...
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }
        ...
        setMeasuredDimension(width, height);
    }

首先:
從父視圖的約束measureSpec獲得約束的模式specMode和約束的大小specSize。
其次:
如果模式為EXACTLY(父控件給子控件決定特定的大?。?,則TextView的寬高直接為父視圖約束的寬高;
如果模式是AT_MOST(子控件至多達(dá)到指定大小的值),則取TextView自身大小與父視圖約束大小的最小值;
如果為UNSPECIFIED(父控件沒有對(duì)子控件施加任何約束,子控件可以得到任意想要的大?。?,TextView沒有額外處理就是取TextView自身的大小。
因此,視圖寬高的計(jì)算是由父視圖傳遞的約束measureSpec決定的。

那么measureSpec又是怎么來(lái)的呢?

wrap_content/match_parent影響measureSpec的計(jì)算

父視圖在自己測(cè)量時(shí)會(huì)調(diào)用子視圖的onMeasure方法,測(cè)量子視圖
例如下面的一個(gè)布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

</LinearLayout>

其測(cè)量的時(shí)序圖:


image.png

在measureChildWithMargins方法中調(diào)用了子視圖的measure方法,最終執(zhí)行了子視圖的onMeasure方法。

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

而子視圖onMeasure方法所需要的measureSpec就是由getChildMeasureSpec方法得到的。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

在getChildMeasureSpec方法中終于見到了久違的wrap_content\match_parent,可以看出子視圖的measureSpec由兩部分決定,父視圖的measureSpec和子視圖的LayoutParams。

如果子視圖的LayoutParams寬高設(shè)置的是具體的大小

...
if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
} 
...

那么子視圖meauserSpec約束模式就是EXACTLY,約束大小就是LayoutParams中定義的大小,根據(jù)前面介紹的TextView的onMeasure方法邏輯,計(jì)算出的大小就是約束的大小。

如果父視圖約束的模式是EXACTLY(通常為父視圖的寬高定義為具體大?。右晥DLayoutParams中定義的是match_parent(占滿父視圖)

...
 case MeasureSpec.EXACTLY:
  if (childDimension == LayoutParams.MATCH_PARENT) {
          // Child wants to be our size. So be it.
          resultSize = size;
          resultMode = MeasureSpec.EXACTLY;
  } 
...

得到的子視圖的measureSpec約束模式還是EXACTLY,約束大小為父視圖約束的大小,TextView計(jì)算的寬高則與父視圖一樣大,實(shí)現(xiàn)占滿父視圖的效果;

如果子視圖定義為wrap_content

...
 case MeasureSpec.EXACTLY:
if (childDimension == LayoutParams.WRAP_CONTENT) {
       // Child wants to determine its own size. It can't be
       // bigger than us.
       resultSize = size;
       resultMode = MeasureSpec.AT_MOST;
}
...

得到的子視圖的measureSpec約束模式為AT_MOST,約束大小為父視圖約束的大小,TextView計(jì)算的寬高則是自身大小與父視圖約束大小的最小值一樣大,實(shí)現(xiàn)由自身大小決定寬高,但不會(huì)超過(guò)父視圖約束大小的效果。

還有很多種組合方式,就不一一列舉,可以看到我們平常記憶理解的wrap_content\match_parent靈活變化視圖寬高的作用效果,正是主要通過(guò)這段代碼邏輯實(shí)現(xiàn)的。

理解視圖測(cè)量時(shí)的一些疑問(wèn):

頂層視圖的measureSpec的由來(lái)

從前面的討論可以知道,測(cè)量方法的參數(shù)measureSpec都是由父視圖傳遞進(jìn)來(lái)的,那么最頂層的視圖的measureSpec是從哪來(lái)的?

我們經(jīng)常給activity設(shè)置的layout布局的根視圖并非是最頂層視圖。
先看一張圖:


image.png

content部分為給activity設(shè)置的布局,其外層還有一個(gè)視圖DecorView,這個(gè)才是最頂層視圖。
而DecorView的測(cè)量過(guò)程是在ViewRootImpl的performTraversals方法中

private void performTraversals() {
    ...
    //獲得view寬高的測(cè)量規(guī)格,mWidth和mHeight表示窗口的寬高,lp.widthhe和lp.height表示  DecorView根布局寬和高
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

    // Ask host how big it wants to be
    //執(zhí)行測(cè)量操作
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //執(zhí)行布局操作
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //執(zhí)行繪制操作
    performDraw();
    ...
}

通過(guò)getRootMeasureSpec方法創(chuàng)建了DecorView的測(cè)量規(guī)格,在performMeasure方法中執(zhí)行了DecorView的測(cè)量,遍歷整個(gè)子視圖。
并且視圖的布局(layout)和繪制(draw)也是從這里發(fā)起的,通過(guò)先后執(zhí)行performMeasure、performLayout、performDraw方法,完成整個(gè)頁(yè)面的繪制過(guò)程

ListView對(duì)子視圖測(cè)量的區(qū)別

ListView的子視圖布局高度無(wú)論是設(shè)置wrap_content還是match_parent,其高度都為子視圖自身的大小,這又是為什么呢?
ListView的中對(duì)子視圖的測(cè)量調(diào)用的是其自己定義的方法measureScrapChild

private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
       LayoutParams p = (LayoutParams) child.getLayoutParams();
        ...
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
        ...
}

當(dāng)子視圖的高度設(shè)置為wrap_content或match_parent時(shí),生成的子視圖測(cè)量模式都是UNSPECIFIED,因此最終計(jì)算出的高度還是子視圖自身的高度。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容