阅读 112

Android 自定义控件 measure

Read The Fucking Source Code

引言

Android自定义控件涉及View的绘制分发流程

源码版本(Android Q — API 29)

前置依赖 【Android 绘制流程】

1. 概览

View绘制流程分发-measure

2. MeasureSpec

2.1 MeasureSpec 简介

  • 测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)。
  • MeasureSpec 中的值是一个整型(32位),高2位是 mode (为什么只用2位?因为只有3个状态,足矣),低30位是 size。这么设计主要是为了内存优化。

2.2 MeasureSpec 三种模式

  • UPSPECIFIED(未指定模式) : 父容器对于子容器没有任何限制,子容器想要多大就多大
  • EXACTLY(精准模式): 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
  • AT_MOST(最大模式):子容器可以是声明大小内的任意大小

2.3 MeasureSpec 从何而来?

2.3.1 最顶层(DecorView)分发的 MeasureSpec

 最顶层分发的 Measure 的只有两种模式:EXACTLY / AT_MOST。(为什么呢?因为 UPSPECIFIED 模式在xml中不存在映射关系,只能在代码中设置,而 DecorView 只是从 xml 加载布局,后面会专门针对UPSPECIFIED 进行说明)

 我们来看 ViewRootImpl 中的 getRootMeasureSpec() 方法

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        //xml中的宽/高设置为MATCH_PARENT
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        //xml中的宽/高设置为WRAP_CONTENT
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        //xml中的宽/高设置为具体值 dp/px
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
复制代码

2.3.2 父 View 对子 View 的 MeasureSpec 计算

 子 View 的 MeasureSpec 值根据子 View 的布局参数(LayoutParams)和父容器的 MeasureSpec 值计算得来的。 子View的MeasureSpec计算

 我们来看 ViewGroup 中的 getChildMeasureSpec() 方法,看不懂不着急,下面有汇总。

//计算子View的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //父view的测量模式
        int specMode = MeasureSpec.getMode(spec);

        //父view的大小
        int specSize = MeasureSpec.getSize(spec);

        //父view出去padding能给到子View的最大值
        int size = Math.max(0, specSize - padding);
        
        //子view想要的实际大小和模式
        int resultSize = 0;
        int resultMode = 0;

        //父View的策略模式
        switch (specMode) {
        // Parent has imposed an exact size on us
         //父View的是精确模式
        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
        //父View的是最大模式
        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
        //父View的是未指定模式
        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的策略模式汇总

2.3.3 子 View 的 MeasureSpec 计算

 我们来看 View 中的 getDefaultSize() 方法

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        //策略模式为UNSPECIFIED时,用自己入参的大小(入参取值简单,不做说明),一般默认为0
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        //策略模式为AT_MOST/EXACTLY时,用父视图的大小
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
复制代码

 由上面的子 View 的 MeasureSpec 计算,可以引出一个问题:

自定义 View 中 如果 onMeasure 方法没有对 wrap_content 做处理,会发生什么?为什么?怎么解决?

  • 如果 View 布局参数设置为 wrap_content,而父视图为 AT_MOST/EXACTLY 时,对应的 View 的 mode 为 AT_MOST。
  • 此时 View 的宽高都返回从 MeasureSpec 中获取到的 size 值(也就是父视图的 size)。
  • 那么 View 的 wrap_content 效果和 match_parent 是一样的。
  • 解决方案就是重写 onMeasure,对 AT_MOST 进行特殊处理,比如给定默认宽高等。

2.3.4 UNSPECIFIED 模式的单独说明

 前面讲了,UNSPECIFIED 模式在 xml 的布局声明中,是没有映射关系的,只能从代码层去设置。具体应用场景有哪些呢?举例:ListView / ScrollView。

 在 ScrollView 中重写了 ViewGroup 的 measureChildWithMargins() 方法。

@Override
    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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        //主动设置子View的策略模式为UNSPECIFIED
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

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

 如果大家以后想自定义控件,比如用 DragHelper 来实现类似 SystemUI 的下拉 StatusBar 的话,就可以使用 UNSPECIFIED 模式,可以完美达到预期效果。

2.3.5 MeasureSpec 小结

View 的 measure 分发,说白了就是不停的计算最终的 MeasureSpec,确定最终的视图尺寸。所以会多次进行measure 计算。这个大家在开发过程中想必也会从 log 中有所体会。

3. measure

3.1 View 和 ViewGroup 的区别

View:测量自己尺寸,然后保存。 ViewGroup:递归遍历所有子 View,测量子 View 尺寸,然后保存;根据所有子 View 的尺寸计算保存自己的尺寸。

3.2 View 和 ViewGroup 的汇总

3.2.1 View 的 measure 过程

View的measure过程

3.2.2 ViewGroup 的 measure 过程

ViewGroup的measure过程

3.3 问题思考

View 的 measure 方法和 onMeasure 方法有什么区别和关系?

  • measure 方法使用了 final 来修饰,说明是不可修改的,onMeasure 方法则是可以让子类按需重写。
  • measure 方法用于检测缓存数据,对比是否要重新测量。
  • onMeasure 方法用于读取父布局的测量规则并按需求定制自己的测量规则,调用 setMeasuredDimension 确定自己的尺寸。

ViewGroup 里面有重写 onMeasure 方法吗?为什么?

  • ViewGroup 默认是没有重写 onMeasure 的,重写 onMeasure 方法这个任务是交给 ViewGroup 的子类的。
  • 不同的 ViewGroup 子类(LinearLayout、FrameLayout 等),它们的布局要求往往都不一样,那 onMeasure 方法就交给他们自己重写好了。

为什么ViewGroup的measure过程不像单一View的measure过程那样对onMeasure做统一的实现?

  • onMeasure()的作用 = 测量View的宽/高值。
  • 因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。
  • 在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需更精细的测量。所以有时候也需重写onMeasure()。
  • 在自定义ViewGroup中,关键在于:根据需求复写onMeasure()从而实现你的子View测量逻辑。

View 的测试方法为什么会给多次调用? View 在什么情况下 getMeasuredWidht/Height() 和 getWidht/Height(),结果是不一致?

  • View 的测量方法会多次调用是因为前后的测量结果可能并不一致,比分说 LinearLayout 的权重,View 的第一次测量结果和最终的测量结果肯定是不一样的。
  • View 的 getMeasuredWidht/Height() 和 getWidht/Height() ,它们大多数情况下是一致的,但是在一种情况下是例外的,那就是在 layout 方法中重新设置 View 的位置大小,从而改变 View 的宽高,因为最终确定 View 的实际尺寸和位置信息的就是 setFrame 方法。
  • getMeasuredWidth方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定。
  • getWidth方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的。
  • 一般情况下在onLayout方法中使用getMeasuredWidth方法,而在除onLayout方法之外的地方用getWidth方法。

小编的扩展链接

《Android 视图模块 全家桶》

优秀博客推荐

Android开发之自定义控件(一)---onMeasure详解
Android开发之getMeasuredWidth和getWidth区别从源码分析
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
Android应用层View绘制流程与源码分析

文章分类
Android
文章标签