View的测量流程

在自定义View的基本流程中,涉及到三个过程:测量、布局和绘制,对应着三个方法:onMeasure()、onLayout()以及onDraw(),接下来将通过2篇文章来介绍这几个方法是如何工作的。

如果不是很清楚View的工作流程,可以先看看方法的调用栈,怎么看?Dubug、自定义一个View在onMeasure()中抛出一个异常,或者在onMeasure()方法中调用Thread.dumpStack(),这几种方式效果都是一样的。这篇文章将从ViewRootImpl的performTraversals()方法开始分析,这个方法内部会依此调用performMeasure()、performLayout()和performDraw()来完成View的测量、布局以及绘制工作,大概的方法的调用过程如下:

(1)测量过程 ViewRootImpl#performMeasure()->measure()->onMeasure()
(2)布局过程 ViewRootImpl#performLayout()->layout()->onLayout()
(3)绘制过程 ViewRootImpl#performDraw()->ViewGroup#draw()->dispatchDraw()->View#draw()->onDraw()

这篇文章先介绍measure过程,也是这三个过程中比较复杂的一个。在View的测量中有一个MeasureSpec的概念,可以理解为测量规格,了解它有助于对测量过程的理解,它的实现如下:

//左移操作是整体左移,超出部分丢弃,低位补0

private static final int MODE_SHIFT = 30;

//11 0000000000 0000000000 0000000000
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

//00 0000000000 0000000000 0000000000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

//01 0000000000 0000000000 0000000000
public static final int EXACTLY     = 1 << MODE_SHIFT;

//10 0000000000 0000000000 0000000000
public static final int AT_MOST     = 2 << MODE_SHIFT;

/**
* 将size和mode组装成一个 MeasureSpec
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        //Android4.2 及更低版本的实现方式
        return size + mode;
    } else {
        //size会将最高的2位置0,mode会将低30位置0,这样做的好处是溢出部分就会被砍掉
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

/**
 * 低30位置0,得到 mode,可见就是makeMeasureSpec的逆过程
 */
public static int getMode(int measureSpec) {
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
}

/**
 * 高2位置0,得到size
 *
 */
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

可见这个MeasureSpec是由mode(高2位)+size(低30位)组成的一个整数,这样做能保证size永远是正数,范围是0~2^30-1,从侧面来看也减少了存储空间。了解了MeasureSpec的概念后,接着就是看View的MeasureSpec是如何确定的,先看看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);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到具体的实现在getChildMeasureSpec()中:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

    //从MeasureSpec中取出父View的specMode和specSize
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    //从这可见ViewGroup默认支持padding
    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) {
        
            //父View的specMode是EXACTLY,子View指定了具体的dp/px
            //最终子View的size就是指定的dp/px,specMode为EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
        
            //父View的specMode是EXACTLY,子View指定为MATCH_PARENT
            //最终子View的size是父View的size,specMode为EXACTLY
            // 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.
            
            //父View的specMode是EXACTLY,子View指定为WRAP_CONTENT
            //最终子View的size是父View的size,specMode为AT_MOST
            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
            
            //父View的specMode是AT_MOST,子View指定具体的dp/px
            //最终子View的size是指定的dp/px,specMode为EXACTLY
            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.
            
            //父View的specMode是AT_MOST,子View指定为MATCH_PARENT
            //最终子View的size是父View的size,specMode为AT_MOST
            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.
            
            //父View的specMode是AT_MOST,子View指定为WRAP_CONTENT
            //最终子View的size是父View的size,specMode为AT_MOST
            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受两个因素影响,分别是父View的specMode和子View的LayoutParams,其中MeasureSpec.UNSPECIFIED主要用于系统内部多次测量的情况,一般来说不需要关注。

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

需要注意的一点是,当子View的LayoutParams为 match_parent、wrap_content时,size都是parentSize,也就是都是match_parent的效果,所以如果你的自定义View要支持wrap_content就要处理一下,比如可以在onMeasure()方法中取出specMode,如果是wrap_content就给它指定一个具体的值就可以,可以看看TextView的源码,它就处理了wrap_content的情况,或者点这里。了解清楚MesureSpec的后,再回过头来看看View测量的具体过程:ViewRootImpl#performMeasure()->measure()->onMeasure()。

/**
* 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.
*
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec)

可以看到measure()被final修饰了,通过其注释也知道,实际的测量工作是在onMeasure(int, int)方法中的,它的实现如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    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;
}

可以看到除了UNSPECIFIED这种specMode,其测量大小就是specSize的大小。UNSPECIFIED这种情况将大小设置size,这个size是getSuggestedMinimumWidth()得到的结果:

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

这个方法的返回值分情况,如果当前View没有设置背景,那返回值就是minWidth的值(默认值是0);设置了背景返回的就是mMinWidth和mBackground中的较大者。其中mBackground.getMinimumWidth()的实现如下:

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

可以看到返回值是一个Drawable的原始宽度或者0,Drawable的getIntrinsicWidth()默认实现是返回-1,具体的值要看具体的Drawable的实现,如ShapeDrawable、BitmapDrawable等,这里就不再深入了。接着看onMeasure()中直接调用的setMeasuredDimension():

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    
    //这里是处理阴影
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

可以看到最后的测量结果是在setMeasuredDimensionRaw()中被记录:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

所以经过测量,我们得到了两个值mMeasuredWidth、mMeasuredHeight,也就是说这个时候我们调用getMeasuredWidth()、getMeasuredHeight()得到的值才是正确的。

以上是View的测量过程,接下来再看看ViewGroup,ViewGrop的是一个抽象类,它没有实现具体的测量工作,它很难去做统一的测量,因为不同的ViewGrop有自己的特性,比如说常用的LinearLayout和FrameLayout就有很大的区别,简单起见,看看FrameLayout的测量过程:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    
    //ViewGroup宽高specMode有一个不是MeasureSpec.EXACTLY,就为true
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    //遍历所有的子View,调用measureChildWithMargins()来测量它们的宽高
    for (int i = 0; i < count; i++) { //①
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            
            //这个for循环结束后,maxWidth就是最大子View的宽度
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {//②
                //如果宽高有一个是MATCH_PARENT就加如集合
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too(把padding加上)
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width(不能小于最小值)
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width(不能小于前景的宽高)
    final Drawable drawable = getForeground(); 
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
    
    //测量当前ViewGoup的宽高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
                    
    //这种类型的child被测量了两次(ViewGroup的specMode不是EXACTLY,子View的宽为LayoutParams.MATCH_PARENT)
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
                //这种类型的View最终的测量值要减去padding和margin,但不能小于0
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }

            final int childHeightMeasureSpec;
            if (lp.height == LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

FrameLayout的测量相对其他的ViewGroup来说测量相对简单一些,从注释①处开始遍历所有的子View,遍历完成后得到最大的子View宽度(包含子View的margin值),接着加上padding,再检查保证不小于最小值。另外要注意的一点是,对于FrameLayout来说如果ViewGroup的specMode不是EXACTLY,子View的宽/高为LayoutParams.MATCH_PARENT,将导致子View被测量两次。可见ViewGroup除了测量自己的宽高外,还要负责子View的测量,换话句话来说就是子View的measure()方法是由父容器调用的。

经过前面的分析,得出View的测量流程大致为:

(1)ViewRootImpl#performMeasure();
(2)先是顶层View DecorView的measure(),这个是个ViewGroup(FrameLayout),测量自己的大小,调用子View的measure();
(3)如果子View也是个ViewGroup就按从步骤(2)递归下去,如果子View是具体的View,就是递归的出口了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容