Android View 工作原理

介紹View的工作原理之前,先來介紹一些基礎的概念,以便后面詳細的介紹View的三大流程:measure、layout和draw。

1.ViewRoot和DecorView

ViewRoot具體對應的類是ViewRootImpl,該類是鏈接WindowManager與DecorView的紐帶,View的三大流程都是通過ViewRoot來完成的。ActivityThread中,Activity被創建之后,會把DecorView添加到Window中,同時創建ViewRootImpl對象,并給ViewRootImpl跟DecorView建立關聯。

View的繪制是從ViewRoot的performTraversals方法開始的,經過measure、layout和draw三個過程View才最終繪制出來。measure用來測量View的寬高,layout用來確定View在父容器中的位置,draw很明顯是把View繪制在屏幕上。

performTraversals流程

如圖所示,performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout、draw這個三個流程。其中:

1.perfromMeasure中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會重復父元素的measure過程,如此反復就完成整個View樹的遍歷。

2.performLayout的傳遞流程和performMeasure是一樣的。

3.performDraw的傳遞過程是在draw方法中通過dispathDraw來實現的,本質上并沒有區別。

measure過程決定了View的寬高,Measure完成以后,可以通過getMeasuredWidth和getMeasuredHeight方法來獲取到View測量后的寬高,在幾乎所有的情況下它都等于View的最終寬高,這僅僅是在代碼規范的前提之下。

layout最終決定了View的四個頂點的坐標和實際View的寬/高,完成以后,可以通過getTop、getBottom、getLeft、getRight來拿到View的四個頂點坐標位置,并可以通過getWidth和getHeight來得到View的最終寬高

draw過程決定了View的顯示,只有draw方法完成以后View的內容才會最終顯示在屏幕上。

簡單介紹了三大過程之后,我們來看下DecorView的結構:

image

DecorView作為頂級View一般會包含一個垂直的LinearLayout,通常在這上面會有上下兩部分(也需要根據具體情況來定),上面是標題欄,下面是內容欄。我們在Activity中setContentView就是添加在內容欄中,內容欄的id是content。大家也可以猜到,View層的時間都是先經過DecorView然后才會傳給其他的View。

2.MeasureSpec

為了更好的了解View的測量過程,我們下面介紹下MeasureSpec。MeasureSpec在很大程度上決定了一個View的尺寸規格,當然除了MeasureSpec還會受到父容器的影響。

MeasureSpec的中文意思是測量規格的意思,MeasureSpec代表一個32的int值,高2位代表SpecMode測量模式,低30位代表SpecSize測量規格大小。

經常使用的三個函數:

1.public static int makeMeasureSpec(int size,int mode)

構造一個MeasureSpec

2.public static int getMode(int measureSpec)

獲取MeasureSpec的測量模式

3.public static int getSize(int measureSpec)

獲取MeasureSpec的測量大小

SpecMode分為三類,每一類都沒有他們對應的約束。

1.UNSPECIFIED

父容器不對View有任何限制,要多大就給多大,這種情況,一般我們自定義View用不到。

2.Exactly

這個表示準確值,這個對應著LayoutParams中的matchparent和準確值,這個時候View的最終大小就是SpecSize所指定的數。

3.AT_MOST

父容器指定了一個可用大小即SpecSize,View的大小不能大于該值,具體是什么要看View中自己的處理,所有自定義View時,我們需要自己在measure里面處理設置布局的大小,它對應layoutparams中的wrap_content

這里還要提一下MeasureSpec跟LayoutParams的對應關系。

使用MeasureSpec來進行View的測量,但是正常情況下我們使用View指定MeasureSpec,盡管如此,但是我們可以給View設置layoutparams,在View測量的時候,系統會將LayoutParams在父容器的約束下自動轉化成對應的MeasureSpec,然后再根據MeasureSpec來確定View最終的寬高。

MeasureSpec不是由LayoutParams唯一決定的,子View的寬高由自身的layoutparams和父容器的MeasureSpec。

對于DecorView,其MeasureSpec由窗口的尺寸和自身的Layoutparams決定;

對于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;

遵循的規則:

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

2.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小

3.固定大小(比如100dp):精確模式,大小為LayoutParams中指定的大小

**4.當view采用固定的寬高時,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精確模式并且大小遵循LayoutParams的大小

5.當View的寬高時matchparent時,如果父容器是精確模式,那么View也是精確模式,并且View也是精確模式并且大小是父容器的剩余空間。如果父容器是最大模式,那么View也是最大模式但是大小不能超過剩余的空間。

6.當View是wrap_content,那么不管父容器的模式,View一定是最大模式,但是不能超過父容器的剩余空間。

image

3.View的工作流程

1.Measure

measure過程要分情況來看,如果只是一個原始的View,那么通過measure方法就完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個流程。下面我們再分別介紹下這兩種情況下的過程。

A.View的Measure過程,上源碼(源碼有方法的注釋,對理解方法有很大的幫助,有興趣可以看下):

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

上面代碼看起來很簡潔,但是我們繼續看里面的getDefaultSize方法。

 /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

這里我們看到了上面setMeasuredDimension設置寬高的測量值的具體方法,getDefaultSize方法邏輯也很簡單,我們重點看下AT_MOST跟EXACTLY這兩種情況。簡單地理解,其實getDefaultSize返回的大小就是measureSpec中的specSize,而這個specSize就是View測量后的大小,這里多次提到測量后的大小,是因為View最終的大小是在layout階段確定的,所以這里必須要加以區分,但是幾乎所有情況下View的測量大小和最終大小是相等的。

UNSPECIFIED這種情況取值是通過getSuggestedMinimumHeight跟getSuggestedMinimumWidth,我們看下源碼:

  /**
     * Returns the suggested minimum height that the view should use. This
     * returns the maximum of the view's minimum height
     * and the background's minimum height
     * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned height is within the requirements of the parent.
     *
     * @return The suggested minimum height of the view.
     */
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

    /**
     * Returns the suggested minimum width that the view should use. This
     * returns the maximum of the view's minimum width
     * and the background's minimum width
     *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned width is within the requirements of the parent.
     *
     * @return The suggested minimum width of the view.
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

以getSuggestedMinimumHeight為例如果View沒有設置背景,那么View的高度為mMinHeight,而mMinWidth對應于android:minHeight這個屬性所指定的值,因此View的寬度即為android:minHeight屬性所指定的值。這個屬性如果不指定,那么mMinHeight則默認為0;如果View指定了背景,則View的寬度為max(mMinHeight,mBackground.getMinimumHeight())。mMinHeight的含義我們已經知道了,那么mBackground.getMinimumHeight()是什么呢?我們看一下Drawable的getMinimumHeight方法

 /**
     * Returns the minimum height suggested by this Drawable. If a View uses this
     * Drawable as a background, it is suggested that the View use at least this
     * value for its height. (There will be some scenarios where this will not be
     * possible.) This value should INCLUDE any padding.
     *
     * @return The minimum height suggested by this Drawable. If this Drawable
     *         doesn't have a suggested minimum height, 0 is returned.
     */
    public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
    }

可以看出,getMinimumWidth返回的就是Drawable的原始寬度,前提是這個Drawable有原始寬度,否則就返回0。那么Drawable在什么情況下會有原始寬度?這個是關于Drawable的問題,我們這里按下不表。

getSuggestedMinimumWidth的邏輯:如果View沒有設置背景,那么返回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設置了背景,則返回android:minWidth和背景的最小寬度這兩者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情況下的測量寬/高。

這里再說明一個問題,從getDefaultSize方法可以看出:直接繼承View的自定義控件需要重寫onMeasure方法并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent。

Why??下面我們分析一下。

如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在這種模式下,它的寬/高等于specSize;根據上面普通View的MeasureSpec創建規則,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩余的空間大小。很顯然,View的寬/高就等于父容器當前剩余的空間大小,這種效果和在布局中使用match_parent完全一致。簡單點說就是你不處理,wrap_content跟match_parent就是一樣的效果。

那要怎么處理呢?我們先看下TextView是怎么做的。

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

        int width;
        int height;
......

這里的widthSize、heightSize是默認值也就是測量大小,width、height是最終值,我們繼續往下看。

if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
            if (mLayout != null && mEllipsize == null) {
                des = desired(mLayout);
            }

            if (des < 0) {
                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                if (boring != null) {
                    mBoring = boring;
                }
            } else {
                fromexisting = true;
            }

            if (boring == null || boring == UNKNOWN_BORING) {
                if (des < 0) {
                    des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, 0,
                            mTransformed.length(), mTextPaint, mTextDir));
                }
                width = des;
            } else {
                width = boring.width;
            }

            final Drawables dr = mDrawables;
            if (dr != null) {
                width = Math.max(width, dr.mDrawableWidthTop);
                width = Math.max(width, dr.mDrawableWidthBottom);
            }

            if (mHint != null) {
                int hintDes = -1;
                int hintWidth;

                if (mHintLayout != null && mEllipsize == null) {
                    hintDes = desired(mHintLayout);
                }

                if (hintDes < 0) {
                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                    if (hintBoring != null) {
                        mHintBoring = hintBoring;
                    }
                }

                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                    if (hintDes < 0) {
                        hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, 0, mHint.length(),
                                mTextPaint, mTextDir));
                    }
                    hintWidth = hintDes;
                } else {
                    hintWidth = hintBoring.width;
                }

                if (hintWidth > width) {
                    width = hintWidth;
                }
            }

            width += getCompoundPaddingLeft() + getCompoundPaddingRight();

            if (mMaxWidthMode == EMS) {
                width = Math.min(width, mMaxWidth * getLineHeight());
            } else {
                width = Math.min(width, mMaxWidth);
            }

            if (mMinWidthMode == EMS) {
                width = Math.max(width, mMinWidth * getLineHeight());
            } else {
                width = Math.max(width, mMinWidth);
            }

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

            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);
            }
        }

這個if/else中間省略了一部分代碼,但是大家可以看到,wrap_content這種情況下,給了一個默認值,計算過程,大家有興趣可以具體看下,其實也就是說,解決這個問題只需要給一個默認值,但是這個默認值大小并沒有固定的依據,我覺得根據自己實際的View的使用情況來定即可。

B.ViewGroup的measure過程

ViewGroup除了完成自己的Measure之外還會遍歷子View的Measure方法,每個子View再遞歸的完成這個過程。ViewGroup是一個抽象類,因此并沒有重寫onMeasure方法,但是提供了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);
            }
        }
    }
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }

可以看到,ViewGroup會對每個子View進行measure,具體可以看下measureChild方法,如上圖。很顯然,measureChild的思想就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec來創建子元素的MeasureSpec,接著將MeasureSpec直接傳遞給View的measure方法來進行測量。getChildMeasureSpec的工作過程已經在上面進行了詳細分析。

ViewGroup的Measure方法可以看下LinearLayout

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
 // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            nonSkippedChildCount++;
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

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

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // 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;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }
......

從上面這段代碼可以看出,系統會遍歷子元素并對每個子元素執行measureChild-BeforeLayout方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,并且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每測量一個子元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的margin等。當子元素測量完畢后,LinearLayout會測量自己的大小,源碼如下所示。

......
 // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds. If we skipped
        // measurement on any children, we need to measure them now.
        int remainingExcess = heightSize - mTotalLength
                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
......
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }
        maxWidth += mPaddingLeft + mPaddingRight;
        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
......

從上面可以看出,當子元素測量完畢后,LinearLayout會根據子元素的情況來測量自己的大小。針對豎直的LinearLayout而言,它在水平方向的測量過程遵循View的測量過程,在豎直方向的測量過程則和View有所不同。具體來說是指,如果它的布局中高度采用的是match_parent或者具體數值,那么它的測量過程和View一致,即高度為specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度總和,但是仍然不能超過它的父容器的剩余空間,當然它的最終高度還需要考慮其在豎直方向的padding,這個過程可以進一步參看如下源碼:

    /**
     * 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.
     * @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:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

View的measure過程是三大流程中最復雜的一個,measure完成以后,通過getMeasured-Width/Height方法就可以正確地獲取到View的測量寬/高。需要注意的是,在某些極端情況下,系統可能需要多次measure才能確定最終的測量寬/高,在這種情形下,在onMeasure方法中拿到的測量寬/高很可能是不準確的。一個比較好的習慣是在onLayout方法中去獲取View的測量寬/高或者最終寬/高。

這里引申一個問題,Activity啟動之后,某個任務需要View的寬高,如何獲取View的寬高呢?

這個場景還是很常見的,我們這里先分析下上面這個問題,我們知道,onCreate、onStart、onResume中均無法正確得到某個View的寬/高信息,因為View的Measure過程跟Activity的生命周期不是同步執行的,有沒有辦法解決這個問題呢?肯定是有的,下面我們就介紹幾種方法。

(1)Activity/View#onWindowFocusChanged。

onWindowFocusChanged這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被調用多次,當Activity的窗口得到焦點和失去焦點時均會被調用一次。具體來說,當Activity繼續執行和暫停執行時,onWindowFocusChanged均會被調用,如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地調用。

2)view.post(runnable)。通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View也已經初始化好了。

3)ViewTreeObserver。使用ViewTreeObserver的眾多回調可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,當View樹的狀態發生改變或者View樹內部的View的可見性發現改變時,onGlobalLayout方法將被回調,因此這是獲取View的寬/高一個很好的時機。需要注意的是,伴隨著View樹的狀態改變等,onGlobalLayout會被調用多次。

(4)view.measure。通過手動對View進行measure來得到View的寬/高。這種方法比較復雜,這里要分情況處理,根據View的LayoutParams來分。

上面四個方法具體可以參考 任玉剛.Android開發藝術探索.電子工業出版社 4.3.1

2.Layout

Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定后,它在onLayout中會遍歷所有的子元素并調用其layout方法,在layout方法中onLayout方法又會被調用。Layout過程和measure過程相比就簡單多了,layout方法確定View本身的位置,而onLayout方法則會確定所有子元素的位置,先看View的layout方法,如下所示。

/**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @SuppressWarnings({"unchecked"})
    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;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            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);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout方法的大致流程如下:

(1)通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值,View的四個頂點一旦確定,那么View在父容器中的位置也就確定了。

(2)接著會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,和onMeasure方法類似,onLayout的具體實現同樣和具體的布局有關,所以View和ViewGroup均沒有真正實現onLayout方法。

接下來,我們可以看一下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:

/**
     * Position the children during a layout pass if the orientation of this
     * LinearLayout is set to {@link #VERTICAL}.
     * @see #getOrientation()
     * @see #setOrientation(int)
     * @see #onLayout(boolean, int, int, int, int)
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    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;
        }
        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();
                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;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
                i += getChildrenSkipCount(child, i);
            }
        }
    }

通過源碼可以看到,此方法會遍歷所有子元素并調用setChildFrame方法來為子元素指定對應的位置,其中childTop會逐漸增大,這就意味著后面的子元素會被放置在靠下的位置,這剛好符合豎直方向的LinearLayout的特性。至于setChildFrame,它僅僅是調用子元素的layout方法而已,這樣父元素在layout方法中完成自己的定位以后,就通過onLayout方法去調用子元素的layout方法,子元素又會通過自己的layout方法來確定自己的位置,這樣一層一層地傳遞下去就完成了整個View樹的layout過程。

下面具體看下setChildFrame:

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

引申一個問題View的測量寬/高和最終/寬高有什么區別?

這個問題實際上是:View的getMeasuredWidth和getWidth這兩個方法有什么區別?回答這個問題我們還是上源碼,看一下getwidth和getHeight這兩個方法的具體實現:

/**
     * Return the width of the your view.
     *
     * @return The width of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getWidth() {
        return mRight - mLeft;
    }
    /**
     * Return the height of your view.
     *
     * @return The height of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getHeight() {
        return mBottom - mTop;
    }

其實看出,在View的默認實現中,View的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成于View的measure過程,而最終寬/高形成于View的layout過程,即兩者的賦值時機不同,測量寬/高的賦值時機稍微早一些。因此,在日常開發中,我們可以認為View的測量寬/高就等于最終寬/高,但是的確存在某些特殊情況會導致兩者不一致,也就是在layout過程中改變了寬高會導致最終的寬高發生變化。

3.Draw

Draw過程就比較簡單了,很明顯它的作用是將View繪制到屏幕上面。先上源碼:

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

可以看到注釋里有View的繪制過程概括下遵循如下幾步:

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

(2)繪制自己(onDraw)。

(3)繪制children(dispatchDraw)。

(4)繪制裝飾(onDrawScrollBars)。

View繪制過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層層地傳遞了下去。View有一個特殊的方法setWillNotDraw。

 /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
...
//ViewGroup
 private void initViewGroup() {
        // ViewGroup doesn't draw by default
        if (!debugDraw()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);
        }
...

從源碼可以看出,如果一個View不需要繪制任何內容,那么設置這個標記位為true以后,系統會進行相應的優化。從上面View跟ViewGroup的代碼也可以看到,View默認是沒有啟用這個優化的,而ViewGroup是默認啟用了。這里就引申出一個問題:當我們的自定義控件繼承于ViewGroup并且本身不具備繪制功能時,就可以開啟這個標記位從而便于系統進行后續的優化。同樣,當明確知道一個ViewGroup需要通過onDraw來繪制內容時,我們需要顯式地關閉WILL_NOT_DRAW這個標記位。

擴展:View的重繪與更新

我們知道View的重繪與更新,可以調用invalidate方法或者是requestLayout方法,兩者有什么區別呢?我們看下源碼注釋:

 /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
    public void invalidate() {
        invalidate(true);
    }

    /**
     * This is where the invalidate() work actually happens. A full invalidate()
     * causes the drawing cache to be invalidated, but this function can be
     * called with invalidateCache set to false to skip that invalidation step
     * for cases that do not need it (for example, a component that remains at
     * the same dimensions with the same content).
     *
     * @param invalidateCache Whether the drawing cache for this view should be
     *            invalidated as well. This is usually true for a full
     *            invalidate, but may be set to false if the View's contents or
     *            dimensions have not changed.
     * @hide
     */
    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
......
 /**
     * Call this when something has changed which has invalidated the
     * layout of this view. This will schedule a layout pass of the view
     * tree. This should not be called while the view hierarchy is currently in a layout
     * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
     * end of the current layout pass (and then layout will run again) or after the current
     * frame is drawn and the next layout occurs.
     *
     * <p>Subclasses which override this method should call the superclass method to
     * handle possible request-during-layout errors correctly.</p>
     */
    @CallSuper
    public void requestLayout() {
......

從上面源碼的注釋中可以看到,invalidate方法是重新繪制更新View,重新調用draw方法。requestLayout方法則是只調用Measure跟Layout方法。invalidate的注釋也寫了,其只能是UI線程調用,非UI線程需要用postInvalidate方法。一般的,View如果appearance發生改變,重新調用invalidate方法就可以,如果View是位置、大小發生變化則需要調用requestLayout。兩者都有變化,一般先調用requestLayout再調用invalidate。

到這里View的工作原理就介紹完了,如果大家想自定義自己的View相信會從上面的介紹中有心得體會。

參考:任玉剛.Android開發藝術探索.電子工業出版社

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容