Android 视图模块

Android 自定义控件 measure

2021-02-04  本文已影响0人  科技猿人

Read The Fucking Source Code

引言

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

源码版本(Android Q — API 29)

本文涉及Android绘制流程

Android 绘制流程

1. 顶层视角预览 measure

2. MeasureSpec

2.1 MeasureSpec简介

2.2 MeasureSpec的三种模式

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值计算得来的。


我们来看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的策略模式汇总


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 过程
3.2.2 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绘制流程与源码分析

上一篇 下一篇

猜你喜欢

热点阅读