Android中View的测量和布局过程

一直以来只是粗略的知道View的绘制会经过measure、layout到最终的draw三个过程,但对其中详细的measure和layout过程一无所知,很影响对一些特殊场景下的布局。

ViewRoot和DecorView

ViewRoot

ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRootImpl来完成的.在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,源码如下:

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
复制代码

View的绘制流程是从ViewRoot的performTraversals方法开始的,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw三大流程. performMeasure会调用measure方法,在measure中又会调用onMeasure方法,在onMeasure中则会对所有子元素进行measure过程,这时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程.接着子元素会重复父容器的measure过程,如此返回就完成了整个View树的遍历. layout和draw同上.

DecorView

DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,LinearLayout中又包含上下两部分,上面是标题栏,下面是内容.在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容中的.DecorView其实是一个FrameLayout,View层的事件都是先经过DecorView,然后才传递给我们的VIew.

Measure

Measure过程如上所述,会由ViewRootImpl发起,从顶层DecorView一层一层传递到最下层.View的宽高会受到父容器限制的影响,而父容器会在调用子View的onMeasure方法时把对子View宽高的限制传递过去.要了解这种限制规则,首先要了解一个类:MeasureSpec.

MeasureSpec

在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据MeasureSpec来测量出View的宽高.

MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小),通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,也提供了打包和解包的方法.

SpecMode

  1. UNSPECIFIED 父容器不对VIew有任何限制,要多大给多大,一般用于系统内部,表示一种测量的状态.

  2. EXACTLY 父容器已经检测出View所需的精确大小,这个时候View的最终大小就是SpecSize所指定的值.对应于LayoutParams中的match_parent和具体的数值这两种模式.

  3. AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体多少要看View的具体实现.对应LayoutParams中的wrap_content.

DecorView的测量

DecorView作为顶级View,和普通View的测量有所不同,其MeasureSpec由窗口尺寸和其自身的LayoutParams来决定,并遵守以下规则:

  • LayoutParams.MATCH_PARENT: 精确模式,大小就是窗口大小;
  • LayoutParams.WRAP_CONTENT: 最大模式,大小不定,但最大不能超过窗口大小;
  • 固定大小: 精确模式:大小为LayoutParams中指定的大小.

普通View的测量

对于普通的View,这里指我们xml布局中的View,View的measure过程由ViewGroup传递而来,先看一下ViewGroup中测量子View的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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
复制代码

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec.很显然子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,具体看一下ViewGroup的getChildMeasureSpec方法:

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

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

        int resultSize = 0;
        int resultMode = 0;

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

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

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

上述方法不难理解,它主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素的可用大小为父容器的尺寸减去padding.

childLayoutParams\parentSpecMode 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

在ViewGroup中计算好父容器期望子View的大小后,此时就调用了子View的onMeasure方法,可以看到onMeasure的默认实现中只是判断了是否有设置背景或最小尺寸限制,如果有,则在无限制模式下将尺寸替换为最小尺寸.这里也能看出minHeight minWidth不是在任何控件中都管用.需要注意的是onMeasure方法没有返回值,需要调用setMeasuredDimension()来保存我们的测量结果.获取测量结果同理,getMeasuredDimension().

ViewGroup并没有onMeasure的实现,一般都由具体的实现类根据自己的业务来实现onMeasure方法,比如LinearLayout.

Measure流程总结

  1. 通过自身的MeasureSpec和子view的LayuoutParams,生成子view的MeasureSpec。这一步调用的是getChildMeasureSpec(int spec, int padding, int childDimension)方法。

  2. 调用子view的measure(int widthMeasureSpec, int heightMeasureSpec)方法,来测量子view的宽高。

  3. 在子view测量结束之后,根据情况来计算自身的宽高。假如自己的MeasureSpec是Exactly的,那么可以直接将SpecSize中的大小作为自己的宽或高;如果是wrap_content或者其他的,那么就需要在每一个子view测量完之后,调用子view的getMeasuredHeight()和getMeasuredWidth()来获得子view测量的结果,然后根据情况计算自己的宽高。

  4. 使用setMeasuredDimension(int measuredWidth, int measuredHeight)方法保存测量的结果。

自定义Measure

我们如果要自定义View的onMeasure过程的话一般有两种方式:

  • 修改测量结果 直接重写onMeasure方法,在super.onMeasure之后修改我们期望的尺寸并保存就好;

  • 自定义测量过程 不调用super.onMeasure,直接计算我们期望的尺寸,并调用resolveSize()来让计算出的尺寸符合父容器的要求,最后别忘了保存.

Layout

Layout过程相对于Measure来说就比较简单了,同样只需要关注Layout()onLayout()方法即可,可以简单理解为在layout方法中确定自己的位置,onLayout方法中确定所有子元素的位置.View和ViewGroup中都有layout(),但都没有实现onLayout(),因为不同类型的布局对onLayout()的要求是不一样的.

当我们自定义了一个ViewGroup的时候,会先确定这个ViewGroup的位置,然后,通过重写 onLayout() 方法,遍历所有的子元素并调用其 layout() 方法,在layout()方法中onLayout()方法又会被调用。ViewGroup就是通过这个过程,递归地对所有子View进行了布局。来看一下View类中的layout()方法的源码:

/**
 * 本方法用来给一个View和它的所有子View设置尺寸和位置;
 * 这是Android布局机制的第二个阶段(第一个阶段是测量);
 * 在这个阶段中,每个父容器都调用layout()方法来定位它的子View;
 * 子类不能重写这个方法,而应该重写onLayout()方法;
 * 在onLayout()方法中调用layout()方法来设置每个子View的位置。
 *
 * @param l 相对于父容器左边的距离
 * @param t 相对于父容器上边的距离
 * @param r 相对于父容器右边的距离
 * @param b 相对于父容器下边的距离
 */
@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<View.OnLayoutChangeListener> listenersCopy =
                    (ArrayList<View.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;
}
复制代码

从源码中可以看出这个方法的大致流程:首先通过setFrame()方法来设置View的四个位置元素的位置,即初始化mLeft、mTop、mRight和mBottom这四个值。View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;接着会调用 onLayout() 方法,这个方法的用途是父容器确定子元素的位置。

在自定义布局的时候,我们的任务就是:遍历所有的子元素,确定它们的大小和位置(大小主要是通过 getMeasuredWidth() 和 getMeasuredHeight() 两个方法,取出在 onMeasure() 方法中测量得到的宽/高;位置需要自行设置),然后调用 view.layout() 方法或直接调用ViewGroup中的方法 setChildFrame() 方法(setChildFrame()方法内部调用的就是view.layout()方法),将子元素布局到这个ViewGroup中。

 最后还需要说明一点,“测量宽/高” 和 “最终宽/高” 是两个不同的概念。测量宽/高是在onMeasure()方法中测量得到的宽度或高度,而最终宽/高是在onLayout()方法中最终放置的子元素的宽度或高度。在View的默认实现中,View的测量宽/高和最终宽/高是相等的,但是测量宽/高的赋值时机较早。

Layout总结

  1. 父layout在自己的onLayout()函数中负责对子view进行布局,安排子view的位置,并且将测量好的位置(上下左右位置)传给子view的layout()函数。

  2. 子view在自己的layout()函数中使用setFrame()函数将位置应用到视图上,并且将新位置和旧位置比较来得出自己的位置和大小是否发生了变化(changed),之后再调用onLayout()回调函数。

  3. 如果此时子view中还有其他view,那么就在自己的onLayout()函数中对自己的子view进行第1补的布局操作,如此循环,只到最后的子view中没有其他view,这样就完成了所有view的布局。

参考:

《Android开发艺术探索》

Android自定义view之measure、layout、draw三大流程

【Android - 自定义View】之View的layout过程解析

猜你喜欢

转载自juejin.im/post/5b56d77cf265da0fa50a1ba1
今日推荐