Android View的测量、布局、绘制过程详解(上)

Android View的绘制过程主要有三步:

  • 测量 Measure
  • 布局 Layout
  • 绘制 Draw

首先理解MeasureSpec的含义,然后跟踪ViewGroup的measure、layout、draw三个方法即可

view的绘制流程是我们在自定义View中通常会使用到的一个知识点,也是一个面试常问的点。简直是Android开发必备知识。

1、理解ViewRootImpl和DecorView两个类

DecorView我们相对比较熟悉,因为开发中就会不时的用到,它是整个Activity的顶层View,我们设置的布局文件都是它的子view。

而ViewRootImpl是连接WindowManager和DecorView的纽带,View的绘制三大流程都是通过ViewRootImpl来完成的。Activity创建后,会把DecorView添加到WindowManager中,同时会创建ViewRootImpl对象,使DecorView和ViewRootImpl建立联系,代码如下:

//代码地址:frameworks/base/core/java/android/view/WindowManagerGlobal.java  addView方法
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

try {
    root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}

View的绘制也是从ViewRootImpl的performTraversals方法开始的,然后调用view的measure、layout、draw方法,流程图如下:

在这里插入图片描述

2、理解MeasureSpec

在真正调用measure方法进行测量前,必须哟啊计算出MeasureSpec,然后才能用MeasureSpec去测出View的大小,那什么是MeasureSpec?

Android系统通过MeasureSpec来测量View的宽高,并且把测量模式和测量数据都放在MeasureSpec中,它由一个32位int值组成,其中高2位代表测量模式,也就是SepcMode,低30位代表测量值,也就是SpecSize,表示View宽或高的具体大小。MeasureSpec是View.java的一个内部类, MeasureSpec以及View的源码路径: /frameworks/base/core/java/android/view/View.java

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    public @interface MeasureSpecMode {}
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

}

MeasureSpec主要就提供了三个方法,上述的makeMeasureSpec方法相当于把mode和size组装在一起,形成一个MeasureSpec。

低30位表示宽或高的大小,那实际可以表示的大小为2^30 -1 ,是一个非常大的值了,完全够用。

高两位代表测量模式,按照00, 01, 10, 11四种组合来看,可以形成四种测量模式,实际上有三种测量模式:

  • UNSPECFIED : 此种模式表示父容器不对View有任何限制,要多大给多大,一般用于系统内部,仅表示一种测量状态;
  • EXACTLY : 表示父容器已经检测出view所需要的精确大小,此时View的最终大小就是SpecSize的值,它对应于LayoutParams中的match_parent或者设置的某个具体数值;
  • AT_MOST: 父容器制定了一个可用的最大数值,但是不知道子View会使用多少,而子View的大小不能超过父容器给的值,但具体也要看View的具体实现,这种模式对应于wrap_content;

3、MeasureSpec的生成过程

3.1 顶级View(即DecorView)的MeasureSpec生成过程

第2节提到, Android系统通过MeasureSpec来进行View的测量,并保存View的宽或高数据。正常情况下,我们在xml布局里面设置宽高或match_parent等, 或者在代码里通过LayoutParams来设置数据, 然后在系统测量View时,系统会将LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后根据这个MeasureSpec测量子view的宽高。注意,是由LayoutParams和父View一起决定View的MeasureSpec。

但是,有一个特殊情况,那就是最顶层的DecorView, 它没有父view,它的MeasureSpec是由手机屏幕的尺寸和自身的LayoutParams来共同决定的。DecorView的MeasureSpec在ViewRootImpl中的measureHierarchy方法中创建:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

其中,desiredWindowWidth和desiredWindowHeight分别表示屏幕的宽高。 测量DecorView的宽高都调用了getRootMeasureSpec:

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

顶级View的测量属性中,测量大小就是屏幕大小,测量模式就是EXACTLY。

3.2普通View的MeasureSpec生成过程

普通View的测量,View的测量是通过ViewGroup传递过来的,因为每个view肯定都是存在于一个ViewGroup中, 先看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);
}

以上代码主要分三步:

  • 1、通过父view宽的MeasureSpec, 然后加上横向的padding和margin,传入childView的宽, 然后算出childView的宽的测量模式MeasureSpec;
  • 2、通过父view高的MeasureSpec, 然后加上纵向的padding和margin,传入childView的高, 然后算出childView的高的测量模式MeasureSpec;
  • 3、通过前面计算的宽高的MeasureSpec,去调用childView的measure方法进行测量,得到真实宽高并记录的childView的MeasureSpec中;

进入getChildMeasureSpec方法查看一下是如何得到childView的测量模式的:

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) {//注释1 通过父view的测量模式,做不同操作
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {//注释2 如果childView的宽高是一个具体的值, 如100dp
            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;//注释3 如果父view是EXACTLY,子view是match_parent, 则子view也是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和子view的LayoutParams, 共同确定子view的测量模式MeasureSpec,总结如下:

  • 1、当View为固定宽高时,测量模式是EXACTLY模式,测量值就是布局参数中的大小。

  • 2、当View为WRAP_CONTENT时,测量模式是AT_MOST模式,测量值是父容器的剩余空间大小。

  • 3、当View为MATCH_PARENT时,测量值是父容器的剩余空间大小,测量模式分两种情况,如果父容器是EXACTLY模式,那就是EXACTLY模式,如果父容器是AT_MOST模式,那么View也是AT_MOST模式。

如第3.2小节开头的代码所展现的,当获取了子view的宽和高的MeasureSpec,就可以真正开始对view进行测量了:

 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

今天先写到这里,具体的measure、layout、draw过程的分析,请见下一篇博客Android View的测量、布局、绘制过程详解(下)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值