Android 開(kāi)發(fā)藝術(shù)探索讀書(shū)筆記 4 -- View 的工作原理(上)

本篇文章主要介紹以下幾個(gè)知識(shí)點(diǎn):

  • 初識(shí) ViewRoot 和 DecorView;
  • 理解 MeasureSpec;
  • View 的工作流程:measure、layout、draw。
hello,夏天 (圖片來(lái)源于網(wǎng)絡(luò))

4.1 初識(shí) ViewRoot 和 DecorView

為更好的理解 View 的三大流程(measure、layout、draw),先了解一些基本的概念。

ViewRoot 對(duì)應(yīng)于 ViewRootImpl 類(lèi),是連接 WindowManagerDecorView 的紐帶,View 的三大流程都是通過(guò) ViewRoot 來(lái)完成的。

View 的繪制流程從 ViewRootperformTraversals 方法開(kāi)始,它經(jīng)過(guò) measure(測(cè)量 View 的寬高),layout(確定 View 在父容器的位置) 和 draw(負(fù)責(zé)將 View 繪制在屏幕上) 三個(gè)過(guò)程才能將一個(gè) View 繪制出來(lái),如下:

performTraversals 的工作流程

DecorView 是一個(gè) FrameLayout,View 層的事件都先經(jīng)過(guò) DecorView,再傳遞給 View。

DecorView 作為頂級(jí) View,一般它內(nèi)部會(huì)包含一個(gè)豎直方向的 LinearLayout,上面是標(biāo)題欄,下面是內(nèi)容欄。在 Activity 中通過(guò) setContentView 設(shè)置的布局文件就是被加到內(nèi)容欄中,而內(nèi)容欄的 id 為 content,可通過(guò) ViewGroup content = findviewbyid(android.R.id.content) 得到 content,通過(guò) content.getChildAt(0) 得到設(shè)置的 View。其結(jié)構(gòu)如下:

頂級(jí) View:DecorView 的結(jié)構(gòu)

4.2 理解 MeasureSpec

MeasureSpec 很大程度上決定了一個(gè) View 的尺寸規(guī)格。在 View 的測(cè)量過(guò)程中,系統(tǒng)會(huì)將 View 的 LayoutParams 根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對(duì)應(yīng)的 MeasureSpec,再根據(jù)這個(gè) measureSpec 來(lái)測(cè)量出 View 的寬高(測(cè)量寬高不一定等于 View 的最終寬高)。

4.2.1 MeasureSpec

MeasureSpec 代表一個(gè)32位 int 值,高兩位代表 SpecMode(測(cè)量模式),低30位代表 SpecSize(某個(gè)測(cè)量模式下的規(guī)格大?。琈easureSpec 內(nèi)部的一些常量定義如下:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

// MeasureSpec通過(guò)將SpecMode和SpecSize打包成一個(gè)int值來(lái)避免過(guò)多的對(duì)象內(nèi)存分配
public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

// 解包:獲取其原始的 SpecMode
@MeasureSpecMode
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

// 解包:獲取其原始的 SpecSize
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

SpecMode 有三類(lèi),其含義分別如下:

  • UNSPECIFIED
    父容器不對(duì) View 有任何的限制(一般用于系統(tǒng)內(nèi)部),表示一種測(cè)量的狀態(tài)

  • EXACTLY
    父容器檢測(cè)出 View 的精度大小,此時(shí) View 的最終大小就是 SpecSize 所指定的值。它對(duì)應(yīng)于 LayoutParams 中的 match_parent 和具體的數(shù)值這兩種模式

  • AT_MOST
    父容器指定一個(gè)可用大小即SpecSize,View 的大小不能大于這個(gè)值。它對(duì)應(yīng)于 LayoutParams 中的 wrap_content

4.2.2 MeasureSpec 和 LayoutParams 的對(duì)應(yīng)關(guān)系

Layoutparams 需要和父容器一起才能決定 View 的 MeasureSpec,一旦確定 MeasureSpec 后,onMeasure 中就可以確定 View 的測(cè)量寬高。

頂級(jí) View(DecorView),其 MeasureSpec 由窗口的尺寸和自身的 Layoutparams 來(lái)共同決定;普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 Layoutparams 來(lái)決定。

對(duì)于 DecorView,在 ViewRootImpl 中的 measureHierarchy 方法中的一段代碼展示了其 MeasureSpec 的創(chuàng)建過(guò)程:

// 其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth , lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接下來(lái)看下 getRootMeasureSpec 方法的實(shí)現(xiàn):

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

上述代碼明確了 DecorView 的 MesourSpec 的產(chǎn)生過(guò)程,根據(jù)其 Layoutparams 的寬高的參數(shù)來(lái)劃分,遵守如下規(guī)則:

  • LayoutParams.MATCH_PARENT
    精確模式,大小就是窗口的大小

  • LayoutParams.WRAP_CONTENT
    最大模式,大小不定,但是不能超出屏幕的大小

  • 固定大?。ū热?00dp)
    精確模式,大小為 LayoutParams 中指定的大小

對(duì)于 普通的 View,指布局中的 View,其 measure 過(guò)程由 ViewGroup 傳遞而來(lái),先看下 ViewGroup 的 measureChildWithMargins 方法:

    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);
        // 調(diào)用子元素的 measure 方法前會(huì)通過(guò)上面的 getChildMeasureSpec 方法得到子元素的 MesureSpec
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述對(duì)子元素進(jìn)行 measure,顯然,子元素的 MesureSpec 的創(chuàng)建和父容器的 MesureSpec 、子元素的 LayoutParams 有關(guān)和 View 的 margin 有關(guān),其中 getChildMeasureSpec 方法如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        // 參數(shù)中的 pading 是指父容器中已占有的控件大小
        // 因此子元素可以用的大小為父容器的尺寸減去 pading
        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);
    }

上述方法主要作用是根據(jù)父容器的 MeasureSpec 同時(shí)結(jié)合 View 本身的 Layoutparams 來(lái)確定子元素的 MesureSpec。

上面getChildMeasureSpec 展示了普通 View 的 MeasureSpec 創(chuàng)建規(guī)則,也可參考下表(表中的 parentSize 指父容器中目前可使用的大?。?/p>

普通 View 的 MeasureSpec 的創(chuàng)建規(guī)則

當(dāng) View 采用固定寬/高時(shí),不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精確模式并且其大小遵循 LayoutParams 中的大小。

當(dāng) View 的寬/高是 match_parent 時(shí),若父容器是精準(zhǔn)模式,那么 View 也是精準(zhǔn)模式并且其大小是父容器的剩余空間;若父容器是最大模式,那么 View 也是最大模式并且其大小不會(huì)超過(guò)父容器的剩余空間。

當(dāng) View 的寬/高是 wrap_content 時(shí),不管父容器的模式是精準(zhǔn)還是最大化,View 的模式總是最大化,并且大小不能超過(guò)父容器的剩余空間。

注:UNSPECIFIED 模式主要用于系統(tǒng)內(nèi)部多次 Measure 的情形,一般不需關(guān)注此模式。

綜上,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地確定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以進(jìn)一步確定出子元素測(cè)量后的大小了。

4.3 View 的工作流程

View 的工作流程主要是指 measure(測(cè)量,確定 View 的測(cè)量寬/高)、layout(布局,確定 View 的最終寬/高和四個(gè)頂點(diǎn)的位置)、draw(繪制,將 View 繪制到屏幕上)這三大流程。

4.3.1 measure 過(guò)程

若只是一個(gè)原始的 View,那么通過(guò) measure 方法就完成了其測(cè)量過(guò)程,若是一個(gè) ViewGroup,除了完成自己的測(cè)量過(guò)程外,還會(huì)遍歷去調(diào)用所有子元素的 measure 方法,各個(gè)子元素再遞歸去執(zhí)行這個(gè)流程。

4.3.1.1 View 的 measure 過(guò)程

View 的 measure 過(guò)程由其 measure 方法來(lái)完成,measure 方法中會(huì)去調(diào)用 View 的 onMesure 方法如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 設(shè)置 View 寬/高的測(cè)量值
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

其中 getDefaultSize 方法如下:

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

上面的 AT_MOSTEXACTLY 這兩種情況,可理解為 getDefaultSize 返回的大小就是 mesourSpec 中的 specSize,而這個(gè) specSize 就是 View 測(cè)量后的大?。y(cè)量大小不一定等于 View 的最終大小)。

至于 UNSPECIFIED 這種情況,一般用于系統(tǒng)內(nèi)部的測(cè)量過(guò)程,View 的大小為 getDefaultSize的第一個(gè)參數(shù)是 size,其寬/高獲取方法如下:

protected int getSuggestedMinimumWidth() {
    // 1. 若 View 沒(méi)有設(shè)置背景,View 的寬度為 mMinwidth,
    // 而 mMinwidth 對(duì)應(yīng)于 android:minwidth 這個(gè)屬性所指定的值,
    // 因此 View 的寬度即為 android:minwidth 屬性所指定的值,
    // 若這個(gè)屬性不指定,那么 mMinWidth 則默認(rèn)為0;
    // 2. 若 View 指定了背景,則View的寬度為max(mMinwidth,mbackground().getMininumwidth)
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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

上面注釋分析了 getSuggestedMinimumWidth 方法的實(shí)現(xiàn),getSuggestedMinimumHeight和它的原理一樣。注釋中未說(shuō)明的 mBackground.getMinimumWidth() 方法(即 Drawable 的 getMinimumWidth方法)如下:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    // 返回 Drawable的原始寬度(有原始寬度的話),否則就返回0
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

總結(jié) getSuggestedMinimumWidth 的邏輯:
若 View 沒(méi)設(shè)背景,那么返回 android:minwidth所指定的值(可為0);
若 View 設(shè)了背景,則返回 android:minwidth和背景的最小寬度這兩者中的最大值。
View 在 UNSPECIFIED 情況下的測(cè)量寬/高即為 getSuggestedMinimumWidthgetSuggestedMinimumHeight的返回值 。

結(jié)論:直接繼承 View 的自定義控件需要重寫(xiě) onMeasure 方法并設(shè)置 wrap_content 時(shí)的自身大小,否則在布局中使用 wrap_content 就相當(dāng)于使用 match_parent。

從上述代碼中知道,若 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,它的寬/高等于 specSize;此情況下 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,即父容器當(dāng)前剩余的空間大小。顯然,View 的寬/高就等于父容器當(dāng)前剩余的空間大小,這種效果和在布局中使用 match_parent 完全一致。

解決上述問(wèn)題代碼如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        // 給 View 指定一個(gè)默認(rèn)的內(nèi)部寬/高(mWidth, mHeight),并在 wrap_content 時(shí)設(shè)置此寬/高即可
        // 對(duì)于非 wrap_content 情形,沿用系統(tǒng)的測(cè)量值即可
        //(注:TextView、ImageView 等針對(duì) wrap_content 情形,它們的 onMeasure 方法做了特殊處理)
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

4.3.1.2 ViewGroup 的 measure 過(guò)程

和 View 不同的是,ViewGroup 是一個(gè)抽象類(lèi),它沒(méi)有重寫(xiě) View 的 onMeasure 方法,但它提供了一個(gè) measureChildren 方法:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        // ViewGroup 在 measure 時(shí),會(huì)對(duì)每一個(gè)子元素進(jìn)行 measure 
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

上述代碼中的 measureChild 方法如下:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        // 1. 取出子元素的 LayoutParams
        final LayoutParams lp = child.getLayoutParams();
        // 2. 通過(guò) getChidMeasureSpec 來(lái)創(chuàng)建子元素的 MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        // 3. 將 MeasureSpec 直接傳遞給 View 的 measure 方法來(lái)進(jìn)行測(cè)量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上面代碼注釋說(shuō)明了 measurechild 的思想。

由于 ViewGroup 是一個(gè)抽象類(lèi),其測(cè)量過(guò)程的 onMeasure 方法需要各個(gè)子類(lèi)去具體實(shí)現(xiàn);不同的 ViewGroup 子類(lèi)有不同的布局特性,它們的測(cè)量細(xì)節(jié)各不相同,如 LinearLayout 和 RelativeLayout 這兩者的布局特性不同,因此 ViewGroup 無(wú)法對(duì)其 onMeasure 方法做統(tǒng)一實(shí)現(xiàn)。


下面通過(guò) LinearLayout 的 onMeasure 方法來(lái)分析 ViewGroup 的 measure 過(guò)程,先來(lái)看一下 LinearLayout 的 onMeasure 方法:

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

這里選擇查看豎直方向的 LinearLayout 測(cè)量過(guò)程,即 measureVertical 方法(其源碼比較長(zhǎng)就不貼了),這里只描述其大概邏輯:系統(tǒng)會(huì)遍歷子元素并對(duì)每個(gè)子元素執(zhí)行 measureChildBeforeLayout 方法,此方法內(nèi)部會(huì)調(diào)用子元素的 measure 方法,當(dāng)子元素測(cè)量完畢之后,LinearLayout 會(huì)根據(jù)子元素的情況來(lái)測(cè)量自己的大小。


View 的 measure 過(guò)程完成后,通過(guò) getMeasureWidth/Height 可以正確地獲取到 View 的測(cè)量寬/高。但在系統(tǒng)要多次 measure 才能確定最終的測(cè)量寬/高的情況下,在 onMeasure 方法中拿到的測(cè)量寬/高可能是不準(zhǔn)確的。因此建議在 onLayout 方法中去獲取 View 的測(cè)量寬/高或者最終寬/高。

問(wèn)題:如何在 Activity 已啟動(dòng)的時(shí)候獲取某個(gè) View 的寬/高?

注:由于 View 的 measure 過(guò)程和 Activity 的生命周期方法不是同步執(zhí)行的,無(wú)法保證 Activiy 執(zhí)行了 onCreate、onStart、onResume 時(shí)某個(gè) View 已經(jīng)測(cè)量完畢了,從而在 onCreate、onStart、onResume 中均無(wú)法正確得View的寬/高信息(若 View 還沒(méi)測(cè)量完畢,那么獲得的寬/高就是0)。

這里給出四種方法:

(1)Activity/View#onWindowFocusChanged

onWindowFocusChanged方法是指:View 已初始化完畢,寬/高已準(zhǔn)備好,此時(shí)去獲取寬/高是沒(méi)問(wèn)題的(注:當(dāng) Activity 繼續(xù)執(zhí)行和暫停執(zhí)行時(shí),onWindowFocusChanged 均會(huì)被調(diào)用,若頻繁地進(jìn)行 onResumeonPause,那么 onWindowFocusChanged 也會(huì)被頻繁地調(diào)用)。典型代碼如下:

    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (!hasFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

(2)view.post(runnable)

通過(guò) post 可將一個(gè) runnable 投遞到消息隊(duì)列的尾部,然后等待 Lopper 調(diào)用此 runnable 時(shí),View 就初始化好了。典型代碼如下:

    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }

(3)ViewTreeObserver

使用 ViewTreeObserver 的眾多回調(diào)可完成這個(gè)功能,典型代碼如下:

    protected void onStart() {
        super.onStart();

        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }

(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)

通過(guò)手動(dòng)測(cè)量 View 的寬高,此方法較復(fù)雜,根據(jù) View 的LayoutParams 來(lái)分情況來(lái)處理:

  • match_parent:無(wú)法測(cè)量出具體的寬高

  • 具體的數(shù)值(dp/px):如寬高都是100dp,如下 measure:

 int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
 int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
 view.measure(widthMeasureSpec, heightMeasureSpec);
  • wrap_content:如下measure:
 // View 的尺寸使用30位的二進(jìn)制表示,即最大是30個(gè)1(即 2^30-1),也就是 (1<<30)-1
 int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
 int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
 view.measure(widthMeasureSpec, heightMeasureSpec);

關(guān)于 View 的 measure,網(wǎng)絡(luò)上有兩個(gè)錯(cuò)誤的用法。為什么說(shuō)是錯(cuò)誤的,首先其違背了系統(tǒng)的內(nèi)部實(shí)現(xiàn)規(guī)范(因?yàn)闊o(wú)法通過(guò)錯(cuò)誤的 MeasureSpec 去得出合理的 SpecMode,從而導(dǎo)致 measure 過(guò)程出錯(cuò)),其次不能保證 measure 出正確的結(jié)果。

  • 第一種錯(cuò)誤的方法:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
  • 第二種錯(cuò)誤的方法:
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

4.3.2 layout 過(guò)程

Layout 是 ViewGroup 用來(lái)確定子元素的位置的,當(dāng) ViewGroup 的位置被確定后,它在 onLayout 中會(huì)遍歷所有的子元素并調(diào)其 layout 方法,在 layout 方法中 onLayout 又被調(diào)用。layout 方法確定 View 本身的位置,而 onLayout 方法則會(huì)確定所有子元素的位置,View 的 layout 方法如下:

     public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        // 1. 通過(guò) setFrame 方法來(lái)設(shè)定 View 的四個(gè)頂點(diǎn)的位置,
        // 即初始化 mLeft,mTop,mRight,mBottom 這四個(gè)值
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 2. View 的四個(gè)頂點(diǎn)一旦確定,那么 View 在父容器的位置也就確定了,
            // 接下來(lái)會(huì)調(diào)用onLayout方法(用途:父容器確定子元素的位置)
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

和 onMeasure 類(lèi)似,onLayout 的具體位置實(shí)現(xiàn)同樣和具體布局有關(guān),所有 View 和 ViewGroup 均沒(méi)有真正的實(shí)現(xiàn) onLayout 方法。 LinearLayout 的 onLayout 如下:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

LinearLayout 的 onLayout 和 onMeasure 的實(shí)現(xiàn)邏輯類(lèi)似,就 layoutVertical 來(lái)說(shuō),其主要代碼如下:

     void layoutVertical(int left, int top, int right, int bottom) {
        . . .

        final int count = getVirtualChildCount();
        // 遍歷所有子元素
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

                . . .

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                // 調(diào)用 setChildFrame 為子元素指定對(duì)應(yīng)的位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

上述方法中的 setChildFrame 方法,僅僅是調(diào)用子元素的 layout 方法而已,如下:

 private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
 }

這樣父元素在 layout 方法中完成自己的定位后,就通過(guò) onLayout 方法去調(diào)用子元素的 layout 方法,子元素又會(huì)通過(guò)自己的 layout 方法來(lái)確定自己的位置,這樣一層一層傳遞下去完成整個(gè) View 樹(shù)的 layout 過(guò)程。

問(wèn)題:View 的測(cè)量寬/高和最終寬/高有什么區(qū)別?(即:View 的 getMeasureWidthgetWidth 這兩個(gè)方法有什么區(qū)別?)

為了回答這個(gè)問(wèn)題,先看下 getWidthgetHeight 方法的實(shí)現(xiàn):

    public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }

可以看出,getWidth 、getHeight 返回的剛好是 View 的測(cè)量寬度、高度。

對(duì)于上面的問(wèn)題:在 View 的默認(rèn)實(shí)現(xiàn)中,View 的測(cè)量寬/高和最終寬/高是相等的,只不過(guò)測(cè)量寬/高形成于 View 的 measure 過(guò)程,一個(gè)是 layout 過(guò)程,而最終寬/高形成于 View 的 layout 過(guò)程,即兩者的賦值時(shí)機(jī)不同,測(cè)量寬/高的賦值時(shí)機(jī)稍微早一些。

日常開(kāi)發(fā)中可用認(rèn)為 View 的測(cè)量寬/高 = 最終寬/高,但某些特殊情況下,如重寫(xiě) View 的 layout 方法如下:

 public void layout(int l,int t,int r, int b){
     super.layout(l, t, r + 100, b + 100);
 }

上述代碼會(huì)導(dǎo)致在任何情況下 View 的最終寬/高總是比測(cè)量寬/高大 100px。

4.3.3 draw 過(guò)程

Draw 過(guò)程其作用是將 View 繪制到屏幕上面。View 的繪制過(guò)程遵循如下幾步:

(1)繪制背景 background.draw(canvas)

(2)繪制自己 (onDraw)

(3)繪制 children (dispatchDraw)

(4)繪制裝飾 (onDrawSrcollBars)

這一點(diǎn)通過(guò) draw 方法的源碼可看出來(lái):

     public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }

        . . .
    }

View 繪制過(guò)程的傳遞是通過(guò) dispatchDraw 來(lái)實(shí)現(xiàn)的,dispatchDraw 會(huì)遍歷所有子元素的 draw 方法,如此 draw 事件就一層層地傳遞下去。View 有一個(gè)特殊的方法 setwilINotDraw

public void setwilINotDraw(boolean willNotDraw){
    // 若一個(gè) View 不需要繪制任何內(nèi)容,那么設(shè)置這個(gè)標(biāo)記位為 true 以后,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化。
    // 默認(rèn)情況下,View 沒(méi)有啟用這個(gè)校化標(biāo)記位,但 ViewGroup 會(huì)默認(rèn)啟用這個(gè)優(yōu)化標(biāo)記位。
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

實(shí)際開(kāi)發(fā)中,自定義控件繼承于 ViewGroup 并且本身不具備繪制功能時(shí),就可以開(kāi)啟這個(gè)標(biāo)記位從而便于系統(tǒng)進(jìn)行后續(xù)的優(yōu)化。若明確知道一個(gè) ViewGroup 需要通過(guò) onDraw 來(lái)繪制內(nèi)容時(shí),需要顯式地關(guān)閉 WILL_NOT_DRAW 這個(gè)標(biāo)記位。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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