View的工作原理--《Android開發藝術探索》閱讀筆記——第四章

1、ViewRoot 和 DecorView

  1. ViewRoot對應ViewRootImpl類,是連接WindowManager和DecorView的紐帶。View的三大流程是通過ViewRoot完成的。 在ActivityThread中,當Activity對象被創建完畢時,會將DecorView添加到Window中,同時會創建ViewRootImpl,且ViewRootImpl和DecorView會建立關聯。如下代碼,WindowManagerGlobal的addView()方法:
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
			...
            root = new ViewRootImpl(view.getContext(), display);
            root.setView(view, wparams, panelParentView);
            ...
            }
  1. View繪製流程從 performTraversals開始,經過Measure、layout、draw。流程圖如下

在這裏插入圖片描述3. DecorView是頂級View,是一個FrameLayout,上面是標題欄、下面是內容欄。內容欄就是setContengView的內容view,id是content。事件 經過DecorView 然後傳給我們自己的View。

2、 MeasureSpec

MeasureSpec封裝了從父級傳遞到子級的佈局要求。系統把view的LayoutParams 根據 父容器施加的規則(父容器的SpecMode) 轉換成 view的MeasureSpec,然後使用這個MeasureSpec確定view的測量寬高(不一定是最終寬高)。

2.1MeasureSpec

1.MeasureSpec—view的測量規格:高2位的SpecMode,低30位的SpecSize。
2.SpecMode的分類
UNPECIFIED父容器對view不限制,要多大給多大,一般系統內部使用。
EXACTLY,父容器檢測出view所需大小,view最終大小就是SpecSize的值。對應 LayoutParams中的matchParent、具體數值 兩種模式。
AT_MOST,父容器制定了可用大小即SpecSize,view的大小不能大於這個值,具體要看view的具體實現。對應LayoutParams中的wrap_content。

2.2MeasureSpec和LayoutParams的對應關係

前面說了View的MeasureSpec是由LayoutParams和父容器的MeasureSpec共同決定。頂級view,即DecorView,是由窗口尺寸和自身LayoutParams決定

1、DecorView,ViewRootImpl中measureHierarchy()方法(performTraversals中執行),代碼如下,desiredWindowWidth、desiredWindowHeight是屏幕的尺寸。

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
            ...
            	childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            	childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            	performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            }

performMeasure()內部是調用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),mView就是DecorVIew。繼續看getRootMeasureSpec()方法如下:

/**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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的MeasureSpec就明確了,根據其LayoutParams:

  • MATCH_PARENT:精確模式,就是窗口大小;
  • WRAP_CONTENT:最大值模式,最大值不能超過窗口大小;
  • 固定值(如100dp):精確模式,就是LayoutParams的指定值。

2、普通View,測量過程從ViewGroup傳遞下來,看ViewGroup的measureChildWithMargins()方法:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    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的MeasureSpec,再調child.measure()。可以看到,child的MeasureSpec是由父容器的MeasureSpec、父容器的padding、child的LayoutParams、child的marging 共同決定。繼續看getChildMeasureSpec()方法:

/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        
        //padding,就是已被佔用的空間,就是 父容器的padding+child的marging
		//size,是ViewGroup本身size減去已使用的空間,是ViewGroup能提供給child的最大值。
        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);
    }

可見,view的MeasureSpec由 viewParent的MeasureSpec和自身layoutParams確定。另外,child的可利用的尺寸是parent尺寸減去padding,上面代碼已有註釋,這很好理解。
梳理如下:

parentSpecMode
/childLayoutParams
EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY
childSize
EXACTLY
childsize
EXACTLY
childsize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

注意,parentSize是父容器可使用的大小。

更新,看到鴻洋公衆號的文章關於UNSPECIFIED說明:

MeasureSpec.UNSPECIFIED是不是真的不常見?

在日常定製View時,確實很少會專門針對這個模式去做特殊處理,大多數情況下,都會把它當成MeasureSpec.AT_MOST一樣看待,就比如最最常用的TextView,它在測量時也是不會區分UNSPECIFIED和AT_MOST的。

不過,雖說這個模式比較少直接接觸到,但很多場景下,我們已經在不知不覺中用上了,比如RecyclerView的Item,如果Item的寬/高是wrap_content且列表可滾動的話,那麼Item的寬/高的測量模式就會是UNSPECIFIED。
還有就是NestedScrollViewScrollView,因爲它們都是擴展自FrameLayout,所以它們的子View會測量兩次,第一次測量時,子View的heightMeasureSpec的模式是寫死爲UNSPECIFIED的。
我們在自定義ViewGroup過程中,如果允許子View的尺寸比ViewGroup大的話,在測量子View時就可以把Mode指定爲UNSPECIFIED。

看到ScrollView重寫了measureChild方法,指定高度的mode是UNSPECIFIED
ScrollView重寫了measureChild方法,指定高度的mode是UNSPECIFIED

3、View的工作流程

View的三大流程,measure、layout、draw。measure確定view的測量寬高,layout確定view的最終寬高和四個頂點位置,draw繪製到屏幕。

3.1 Measure過程

view的測量過程,由measure()方法完成。viewGroup測量自身後,還需調用child.measure()遍歷測量子view。

3.1.1 view的測量過程

/**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * </p>
     *
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     *
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     *
     * @see #onMeasure(int, int)
     */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	...
	// measure ourselves, this should set the measured dimension flag back
     onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

可見view的measure()方法是final,不可被子類重寫。裏面調用onMeasure(),實際真正的測量過程在onMeasure()中。所以只有onMeasure()可以且必須被子類重寫。另外,參數widthMeasureSpec、heightMeasureSpec就是上一節最後的表格中的值。繼續看onMeasure():

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

從名字就可以看出,setMeasuredDimension()就是設置測量的尺寸,且在onMeasure()中必須被調用,否則在測量時會發送異常。getDefaultSize()獲取默認的寬/高。所以View類中的onMeasure() 是設置默認的寬高。 繼續看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;
    }

UNSPECIFIED,一般是系統使用,不需要關心。這裏view大小直接取size,就是getSuggestedMinimumWidth()/getSuggestedMinimumHeight(),意思是 建議的 最小寬高。看下實現:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

沒有背景,就取mMinWidth,就是xml中設置的minWidth屬性值;有背景,取 mMinWidth 、背景的MinimumWidth 的較大值。drawable的getMinimumWidth()如下,有固有寬度就取固有寬度(如BitmapDrawable),沒有就是0(如ShadeDrawable)。

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

AT_MOST、EXACTLY,直接取specSize,就是上一節最後的表格中的值,作爲測量寬高。那這樣取specSize是否合適呢? 再來看一遍specSize的來源。

parentSpecMode
/childLayoutParams
EXACTLY AT_MOST UNSPECIFIED
dp/px 1EXACTLY
childSize
2EXACTLY
childsize
EXACTLY
childsize
match_parent 3EXACTLY
parentSize
4AT_MOST
parentSize
UNSPECIFIED
0
wrap_content 5AT_MOST
parentSize
6AT_MOST
parentSize
UNSPECIFIED
0

1、2的情況,具體dp值,取SpecSize沒問題,因爲是EXACTLY,就是給定的的尺寸。
3的情況,match_parent,取SpecSize,即parentSize,也沒問題,因爲是EXACTLY,也是確定的尺寸。
4的情況,match_parent,但父容器又是wrap_content,系統就給了AT_MOST+parentSize,限制最大尺寸爲parentSize。而這裏直接取specSize即parentSize,似乎也沒問題。這個看一個例子一,如下,view是match_parent,可見view取得確實是parentSize。在這裏插入圖片描述5、6的情況,wrapContent即AT_MOST+parentSize,取specSize也就是parentSize,所以和3、4一樣都是parentSize,即View類 中 默認wrapContent等同於match_parent

再看一個情況例子二,如下,View換成TextView(繼承View),尺寸就不是parentSize了,而是內容尺寸,說明TextView在onMeasure中做了處理。
在這裏插入圖片描述繼續看,例子三如下,同時有TextView、View,此時textView又是取parentSize(可用空間):
在這裏插入圖片描述所以得出結論:
通常直接繼承View的自定義View,在onMeasure()需要處理 :
a、wrap_content的情況,否則wrap_content就等同於match_parent;
b、match_parent+父容器wrap_content的情況,否則就像例子一,父容器wrap_content是無效的,處理方式就是例子二中的textView。
總結就是,直接繼承View的自定義View,需要處理AT_MOST時的寬高

處理方式如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, mHeight);
        }
    }

實際就是在 AT_MOST時 設置一個指定的尺寸mWidth、mHeight,其他情況沿用系統。至於mWidth、mHeight是多少,則要具體看你的view的邏輯了。例如TextView,可以參考其源碼的實現。

3.1.2 ViewGroup的測量過程

ViewGroup需要完成自身的測量,還要遍歷子view調用measure()方法進行測量。

ViewGroup是抽象類,沒有重寫onMeasure,因爲無法做到統一,是讓具體繼承ViewGroup的子類重寫自己的邏輯。但是提供一些方便的方法給子類調用。如measureChildren()、measureChild()、measureChildWithMargins(),上面第二節分析過measureChildWithMargins(),這裏我們看下measureChildren():

/**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

就是遍歷子view,調用measureChild(),繼續看:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

通過getChildMeasureSpec()獲取child的MeasureSpec,然後調用child.measure(),測量就傳到child內部了,很好理解。measureChild()相比measureChildWithMargins() 沒有考慮child的margin值。

上面說了,ViewGroup沒有重寫onMeasure,因爲無法做到統一,讓具體繼承ViewGroup的子類重寫自己的邏輯。具體看下LinearLayout的測量過程

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

繼續看measureVertical():

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	...
	//下面這句官方註釋:看每個人多高,也記住最大寬度。想想這不就是計算豎向LinearLayout寬高的思路嘛!
	// See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
        	...
            final View child = getVirtualChildAt(i);
            ...
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            ...
            	// Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                //這裏測量child(裏面就是measureChildWithMargins())
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                ...
                final int totalLength = mTotalLength;
                //這裏mTotalLength加上child的高度、margin,就是child高度累積。
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
                ...
                //這裏記錄最大寬度(包含margin)
                final int margin = lp.leftMargin + lp.rightMargin;
            	final int measuredWidth = child.getMeasuredWidth() + margin;
            	maxWidth = Math.max(maxWidth, measuredWidth);
            ...
        }
        //遍歷完了:高度加上自身的上下padding
		// Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
        
        //這裏很重要:調用resolveSizeAndState--決定 計算的高度(高度累加)和 LinearLayout的父容器約束的高度,取哪一個。
        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
	...
		//最大寬度加上左右margin
		maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

		//設置最終的測量尺寸(寬也也同樣調用resolveSizeAndState決定取哪個)
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
}

所以,簡單概括就是:
1.先測量所有child;
2.根據child的情況獲取自身寬高(累加高度、最大寬度)。

那麼,是否就取 累加高度、最大寬度?再看下resolveSizeAndState():

/**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Will take the desired size, unless a different size
     * is imposed by the constraints. The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
     * resulting size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be. --想要的尺寸
     * @param measureSpec Constraints imposed by the parent. --父佈局給的measureSpec
     * @param childMeasuredState Size information bit mask for the view's
     *                           children.
     * @return Size information bit mask as defined by
     *         {@link #MEASURED_SIZE_MASK} and
     *         {@link #MEASURED_STATE_TOO_SMALL}.
     */
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
            	//AT_MOST時,想要的尺寸大於約束的尺寸,就只能取 約束的尺寸。
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
            	//dp值、match_parent且父EXACTLY,就是SpecSize
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

這個過程就是限制 AT_MOST時,即wrap_content(或match_parent且父wrap_content)時高度不能大於parent的剩餘空間

3.1.3 獲取View寬高的時機

Measure過程完成,就可通過getMeasuredWidth()、getMeasuredHeight()獲取測量寬高。但某些極端情況
需要多次Measure才能確定最終寬高。所以在onLayout方法中獲取測量寬高是真正ok的。
我們知道,activity的onCreate中無法獲取到view的寬高。實際onCreate、onStart、onResume都不能保證view已完成測量,所以可能獲取的都是0。因爲view的measure和activity生命週期不是同步的。

以下是保證可以獲取view測量寬高的方法

1、Activity/View # onWindowFocusChanged

onWindowFocusChanged:View已初始化完畢,寬高已準備ok。 但會多次調用,獲取焦點、失去焦點都回調用。(這個回調是ViewRootIml中分發到DecorView,接着到Activity、到各級View。)

@Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int measuredWidth = scoreView.getMeasuredWidth();
            int measuredHeight = scoreView.getMeasuredHeight();
        }
    }

2、view.post(runnable)

view.post可以把runnable放入消息隊列,等待looper到此runnable是view已經初始化完成。v詳細原理參考【Android源碼解析】View.post()到底幹了啥

@Override
    protected void onStart() {
        super.onStart();
        scoreView.post(new Runnable() {
            @Override
            public void run() {
                int measuredWidth = scoreView.getMeasuredWidth();
                int measuredHeight = scoreView.getMeasuredHeight();
            }
        });
    }

3、ViewTreeObserver

ViewTreeObserver有很多回調,其中有個OnGlobalLayoutListener,當View樹的狀態發生改變或者View樹內部view的可見性發生改變時 方法 onGlobalLayout()都會被調用。所以是會回調多次。 此時也可以獲取view的寬高:

ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                if (mIsGroupListAnimating) {
                    mIsGroupListAnimationPending = true;
                } else {
                    updateLayoutHeightInternal(animate);
                }
            }
        });

3.2Layout過程

layout()的作用是View用來確定view本身位置,內部調用onLayout()來確定子view的位置。 layout過程比measure過程簡單很多。看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;
        //使用setFrame方法設置4個頂點,就確定位置了~
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        	//這裏調用onLayout,是個空實現。ViewGroup中重寫了,還是空實現,但加了abstract,即ViewGroup的子類必須重寫onLayout確定子View的位置。
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

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

        ...
    }

先是用setFrame方法設置4個頂點,就確定位置了,即mLeft、mTop、mBottom、mRight確定了。 然後調用onLayout,是個空實現。ViewGroup中重寫了onLayout,還是空實現,但加了abstract,即ViewGroup的子類必須重寫onLayout確定子View的位置
那就看看LinearLayout的onLayout

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

繼續看layoutVertical():

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
		//遍歷子view
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
            	//獲取child的測量寬高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

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

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

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

                childTop += lp.topMargin;
                //以上就是獲取子view的左、上的位置,即寬高,然後調用setChildFrame
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                 //top位置加上高度和margin,就是下一個view的top
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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

就是遍歷子view,確認childLeft、childTop,調用setChildFrame確認子view的位置:

private void setChildFrame(View child, int left, int top, int width, int height) {
		//這裏width、height就是 上面獲取的 測量寬高
        child.layout(left, top, left + width, top + height);
    }

也就是調用child的layout方法,這樣就走child的layout過程了。

一個問題:getMeasuredWidth() 與 getWidth()有何區別?
答曰:一般情況,getMeasuredWidth() 與 getWidth()兩者無區別
先看,getWidth():

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

在上面分析LinearLayout時,child.layout的參數中 mRight就是mLeft + measuredWidth,所以getWidth()就是measuredWidth。只不過是measuredWidth在測量過程產生,getWidth()在layout過程產生。 只要不重寫view的layout()方法(也不需要重寫)改變頂點位置就不會出現不同的情況,例如下面這個最終寬高比測量寬高大100。

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

3.3Draw過程

draw過程:
1、畫背景
2、畫自己-- onDraw,自己實現
3、畫子view-- dispatchDraw
4、畫裝飾

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

            drawAutofilledHighlight(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);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

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

ViewGroup一般不用onDraw畫自己,只需要畫子View就可以了。但明確需要畫自己的話,需要調用setViewNotDraw(false);

以上View的三大流程就分析完了。

4、自定義View

自定義view涉及view層次結構、事件分發、工作原理,有一定複雜度,但也是有章可循的。

4.1自定義view的分類

  1. 繼承View:重寫onDraw,要處理wrap_content、padding。
  2. 繼承ViewGroup:重寫onMeasure測量自己、子View,重寫onLayout佈局子View。
  3. 繼承特定View(如TextView):擴展自己的功能。
  4. 繼承特定ViewGroup(如LinearLayout):擴展自己的功能。

4.2 自定義view 注意點

  1. 支持wrap_content:直接繼承View或ViewGroup的,要在onMeasure中處理wrap_content的情況。
  2. 支持padding:直接繼承View在onDraw中處理;直接繼承ViewGroup,在onMeasure、onLayout中處理padding和子view的margin。
  3. 不要在View中使用handler,因爲本身提供了post方法。
  4. 在View#onDetachedFromWindow中停止動畫或線程。
  5. 處理好嵌套滑動。

4.3 例子

自定義ViewGroup實例:橫向滑動HorizontalView

4.4 自定義view的思想

先掌握基本功,彈性滑動、滑動衝突、繪製原理等,然後選擇自定義的類別,按照注意事項多做就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章