美文网首页Android自定义View
Android 源码分析二 View 测量

Android 源码分析二 View 测量

作者: lovejjfg | 来源:发表于2019-06-10 22:35 被阅读1次

    第一篇说完 View 创建,接着讲讲 View 的测量和布局。先讲讲整体思想,View 的 测量是自上而下,一层一层进行。涉及到的核心方法就是 View 中的 measure() layout() 对于我们来说,更应该关心的就是 onMeasure()onLayout() 的回调方法。本文着重关注测量相关代码,至于 layout ,这个是 ViewGroup 的具体逻辑。

    onMeasure

    说到 onMeasure() 方法就必须提一嘴它涉及到的测量模式。以及模式对子 view 的约束。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    这是 ViewonMeasure() 方法默认实现,这里又涉及到三个重要的方法, setMeasuredDimension()getDefaultSize()
    setMeasuredDimension() 这个方法非常重要,它是我们设置测量宽高值的官方唯一指定方法。也是我们在 onMeasure() 方法中必须调用的方法。如果你想了下,自己似乎在 onMeasre() 没有手动调用过该方法,并且也没有啥异常,不要犹豫,你一定是调用了 super.onMeasure() ,setMeasuredDimension()最终会完成对 measureHeightmeasureWidth 赋值,具体操作往下看。

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
    
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    

    setMeasuredDimension() 中调用私有的 setMeasuredDimensionRaw() 方法完成对 mMeasuredWidthmMeasuredHeight 赋值,然后更新 flag 。

    getSuggestedMinimumWidth/Height

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

    这两个方法的默认实现就是去获取 View 设置的背景和最小值中最小的那个。背景设置就不用说了,至于这个宽高最小值,其实就是通过 xml 中 minWidth 或者 API 动态设置。

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

    这个方法也比较重要,因为它涉及到测量模式。先分析下参数,输入的第一个 size 是刚刚获取的最小值。第二个就是父布局回调过来的测量参数。

    通过上面可以看到,测量模式一共有三种。MeasureSpec.UNSPECIFIED MeasureSpec.AT_MOST MeasureSpec.EXACTLY

    如果是 MeasureSpec.UNSPECIFIED ,那么就直接使用获取的最小值。如果是其他两种模式,那么就从测量参数中获取对应的 size。注意,在这个方法中,根本没有对 AT_MOST 和 EXACTLY 做区分处理。

    MeasureSpec 测量模式和size

    通过上面 getDefaultSize() 方法我们已经看到 MeasureSpec 中包含有测量模式和对应 size。那么它是怎么做到一个 int 类型,表示两种信息呢?程序员的小巧思上线。

    一个 int 类型,32位。这里就是使用了高2位来表示测量模式,低 30 位用来记录 size。

    //左移常量 shift 有转变的意思 而且在 Kotlin 中 左移使用 shl() 表示
    private static final int MODE_SHIFT = 30;
    //二进制就是这样11000...000
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //00 000...000
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //01 000...000
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //10 000...000
    public static final int AT_MOST     = 2 << MODE_SHIFT;  
    

    接着看看是怎么赋值和取值的呢。

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                        @MeasureSpecMode int mode) {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
    

    看着是不是比较高大上?都说了这是程序员小巧思,代码当然比较溜。这里涉及到与或非三种运算。直接举个例子吧,比如我要创建一个 size 为 16 模式是 EXACTLY 的 MeasureSpec 那么就是这样的。

        size    对应   00 000... 1111
        mode    对应   01 000... 0000
        mask    对应   11 000... 0000
        ~mask   对应   00 111... 1111
        size & ~mask  00 000... 1111 = size
        mode & mask   01 000... 0000 = mode
        size | mode   01 000... 1111 = 最终结果
    

    通过这么一对应,结果非常清晰,有没有觉得 makeMeasureSpec() 方法中前两次 & 操作都是很无效的?其实它能保证 mode 和 size 不越界,不会互相污染。反正你也别瞎传值。赋值时,方法上已经对两个参数都有输入限制。

    再说完三种模式定义之后,接着就需要考虑 xml 中的 宽高指定最后是怎么转换为对应的 模式。比如说,我们写 wrap_content, 那么对应的测量模式到底是怎样的呢?

    举个例子,比如说如下的一个布局。

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/parent_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:background="@color/colorAccent">
    
        <ProgressBar
            android:id="@+id/child"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_gravity="center"
            android:indeterminate="true"
            android:indeterminateTint="@color/colorPrimary"
            android:indeterminateTintMode="src_in"/>
    
    </FrameLayout>
    

    效果通过预览就能看到,FrameLayout 占据全屏,ProgressBar 居中显示,size 就是 20 dp 。

    ProgressBaronMeasure() 方法如下:

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int dw = 0;
        int dh = 0;
    
        final Drawable d = mCurrentDrawable;
        if (d != null) {
            dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
            dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
        }
    
        updateDrawableState();
    
        dw += mPaddingLeft + mPaddingRight;
        dh += mPaddingTop + mPaddingBottom;
    
        final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
        final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
    

    可以看到,ProgressBar 复写了 View 的 onMeasure() 方法,并且没有调用 super 。所以,最上面那一套分析对于它无效。因此,它也自己在最后调用了 setMeasuredDimension() 方法完成一次测量。在这里,又涉及到一个 View 的静态方法 -- resolveSizeAndState()

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
    

    入参 size 是背景大小,MeasureSpeconMeasure() 方法传入,参数由 parent 指定。 state 的问题先不考虑,我们这里主要看 size 。对比刚刚说过的 getDefaultSize() , 这个方法已经将 AT_MOSTEXACTLY 做了区分处理,一共又四种情况。

    AT_MOST 下,如果测量值小于背景大小,即 View 需要的 size 比 parent 能给的最大值还要大。这个时候还是设置为 测量值,并且加入了 MEASURED_STATE_TOO_SMALL 这个状态。如果测量值大于背景大小,正常情况也就是这样,这时候就设置为背景大小。EXACTLY 下,那就是测量值。UNSPECIFIED 下,就是背景 size。

    数值传递

    上面其实都是说的是 ViewonMeasure 中测量自己的情况,但是,parent 传入的 MeasureSpec 参数到底是怎么确认的呢?child 设置 match_parent 或者 wrap_content 或者 精确值,会影响对应的 MeasureSpec 的模式和 size 吗?

    带着这些问题,我们看看 FrameLayoutonMeasure() 方法的部分实现。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        // 如果自己的宽高有一个不是精确值,measureMatchParentChildren flag 就 为 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;
    
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 通过自己的 MeasureSpec 测量child
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                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());
                // 如果 child 是 match_parent 但是 自己又不是一个精确值,那就要重新再次测量
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
    
        // Account for padding too
        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());
        }
        // 通过上面的步骤,拿到了最大的宽高值,调用 setMeasuredDimension 确定自己的size
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        // 最后,之前有指定 match_parent 的 child 需要根据最新的宽高值进行再次测量
        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) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    // match_parent 的状态更改为 精确值
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    // 其他情况 getChildMeasureSpec() 重新确定 MeasureSpec
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
                // 确定高度代码同上,省略
                ...
    
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
    

    在第一次测量 child 时,调用了 measureChildWithMargins() 方法,该方法中,最后会调用 getChildMeasureSpec() 方法,在第二次确认宽高时,也是通过这个方法确定相关的 MeasureSpec 。 可以看出,getChildMeasureSpec() 是一个非常重要的静态方法。它的作用是根据 parent 的相关参数 和 child 的相关参数,确定 child 相关的 MeasureSpec 生成。在这里,三种测量模式和 xml 中的 match_parent wrap_content 或者 具体值 在这里产生关联。

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

    代码是这样的,为便于理解,制作了以下这个表格,可以对号入座。

    Parent(pSize)
    ------
    Child(size)
    EXACTLY AT_MOST UNSPECIFIED
    EXACTLY EXACTLY (size) EXACTLY (size) EXACTLY (size)
    MATCH_PARENT EXACTLY (pSize) AT_MOST (pSize) UNSPECIFIED (pSize)
    WRAP_CONTENT AT_MOST (pSize) AT_MOST (pSize) UNSPECIFIED (pSize)

    通过这个方法,就生成了最后用于测量 child 的相关 MeasureSpec 。接着就可以调用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 让 child 开始测量自己,最后就会回调到 child 的 onMeasure() 方法中。

    上面这个布局,如果直接 setContentView() 加载的话,那么在 FrameLayout 中,FrameLayoutMeasureSpecEXACTLY + pSize 这种情况。

    LayoutParameter 特征类

    上面的宽高信息是从 LayoutParameter 这个类中取出来的。 这个类可以说是相当重要,没有它的话,我们写的 xml 相关属性就无法转化为对应的代码。在这里继续抛出一个问题,在 LinearLayout 布局中我们可以直接使用 layout_weight 属性,但是如果改为 FrameLayout 之后,这个属性就会没效果;同时,FrameLayout 中定义的 gravity 属性,在 LinearLayout 中也没有效果。为什么呢?代码层面到底实现的呢?

    这就是 LayoutParams 的作用,LayoutParameter 定义在 ViewGroup 中,是最顶级,它有很多子类,第一个子类就是 MarginLayoutParams ,其他具体实现跟着具体的 ViewGroup ,比如说 FrameLayout.LayoutParameter LinearLayout.LayoutParameter 或者 RecyclerView.LayoutParameter

    ViewGroup 中定义了生成 LayoutParams 的方法 generateLayoutParams(AttributeSet attrs)

    // ViewGroup
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
    
    //ViewGroup.LayoutParams
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }
    
    //ViewGroup.LayoutParams
    protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
        width = a.getLayoutDimension(widthAttr, "layout_width");
        height = a.getLayoutDimension(heightAttr, "layout_height");
    }
    

    通过上面的代码,所有的 ViewGroup 都有 generateLayoutParams() 的能力。在默认的 ViewGroup 中,它只关心最基础的宽高两个参数。接着对比 FrameLayoutLinearLayout, 看看相关方法。

    //FrameLayout.LayoutParams
    public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
        super(c, attrs);
    
        final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
        gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
        a.recycle();
    }
    //LinearLayout.LayoutParams
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a =
                c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
    
        weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
        gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
    
        a.recycle();
    }
    

    可以看到,在 FrameLayout 中 额外解析了 gravity ,在 LinearLayout 中 额外解析了 weightgravity

    视图异常原因

    回到上篇文章 View 的创建过程中的 Layoutinflater.inflate() 方法。

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            try {
                ...
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
                    ViewGroup.LayoutParams params = null;
    
                    if (root != null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                   ...
                }
        }
    }
    

    这里有一个大坑需要填一下。LayoutInflater.inflate() 方法中,需要我们指定 parent ,如果不指定,会出现啥情况呢,就是 LayoutParams 没有被创建出来。最后在 addView() 方法中:

    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        // inflate 的时候并没有生成相关 LayoutParams
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            // 没有生成的话,就创建一个 default LayoutParams
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
    
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    

    默认的 LayoutParams 只会设置 宽高信息,至于刚刚说的 gravity weight 这些属性就被丢弃,如果你 inflate() 的顶层布局真的带有这些属性,不好意思,就这样丢失了。
    这也是有人抱怨自己 inflate 布局时,布局样式异常的一个重要原因。要避免这个问题,就要做到 inflate 时一定要传入对应的 parent 。不要有 inflate(R.layout.xx,null) 这种写法,而且这种写法,目前 Studio 会直接警告你。

    inflate 的场景其实不太多,在 Fragment 或者 创建 ViewHolder 时,系统都会将对应的 parent 传给你,这个好解决。但是在使用 WindowManager.addView() PopupWindow Dialog 时,可能不好找到对应的 parent。这时候咋办呢?这个时候可以拿 window.decorView 或者,你直接 new 一个具体的 ViewGroup 都行。

    到这里,关于 LayoutParams 似乎就说完了。 inflate() 这个大 bug 似乎也解决了。

    Dialog 视图异常

    创建过 Dialog 或者 DialogFragment 的小伙伴都清楚,Dialog 布局中,你写 match_parent 是没有效果,结果总是 wrap_content 的样子。通过上面一波分析,一开始我以为是 inflate 那个错误,然后,即使我指定上对应的 parent ,想当然以为布局可以符合预期。结果还是老样子。

    为什么会这样呢?

    这又要回到刚刚上面 getChildMeasureSpec() 方法和表格中。我们每次写 match_parent 时,默认 parent 是什么 size 呢?当然想当然就是屏幕宽高那种 size。
    Dialog 中,会创建对应的 PhoneWindowPhoneWindow 中有对应的 DecorViewDecorView 并不是直接添加我们布局的根 View,这里还有一个 mContentParent ,这才是展现我们添加 View 的直接领导,老爹。在 PhoneWindow 中创建 mContentParent 时,有这么一个判断。

    protected ViewGroup generateLayout(DecorView decor) {
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        }
    }
    

    而我们使用各种样式的 Dialog 时,其实会加载默认的 style ,最基本的 dialog style 中,分明写了这么一个默认属性。

    <style name="Base.V7.Theme.AppCompat.Light.Dialog" parent="Base.Theme.AppCompat.Light">
        ...
        <item name="android:windowIsFloating">true</item>
        ...
    </style>
    

    这两个代码放一块,问题开始转化。当 parent(decorView) 为 精确值,child(mContentParent) 为 wrap_content 时,最后在 child 中对应的 MeasureSpec 是什么样呢?
    查上面的表就知道,这个时候的 child measureSpec 应该是 AT_MOST + pSize
    当 parent (mContentParent) 为 AT_MOST ,child (填充布局) 为 match_parent 时,最后 child 中对应的 MeasureSpec 是什么样呢?
    继续查表,显然,这里也是 AT_MOST + pSize 这种情况。注意,这里就和上面第一次分析的 EXACTLY + pSize 不一样了。

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/parent_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:background="@color/colorAccent"
        android:clipChildren="false">
    
        <ProgressBar
            android:id="@+id/progressbar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:indeterminate="true"
            android:indeterminateTint="@color/colorPrimary"
            android:indeterminateTintMode="src_in"/>
    
    </FrameLayout>
    

    假设在 Dialog 中我们就填充如上布局。结合上面 FrameLayout 分析, child 的 size 要再次测量。关键在 FrameLayout onMeasure() 方法最后的 setMeasuredDimension()方法中会调用 resolveSizeAndState()

    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
    

    第一次是 EXACTLY ,所以就是 pSize 。 这一次是 AT_MOST ,所以就成了 childSize 。那最后效果其实就是 wrap_content 。到这里 Dialog 显示异常从代码上分析完成。那么需要怎么解决呢? 首先可以从根源上,将 windowIsFloating 设置为 false 。

    //styles.xml
    <style name="AppTheme.AppCompat.Dialog.Alert.NoFloating" parent="Theme.AppCompat.Light.Dialog.Alert">
        <item name="android:windowIsFloating">false</item>
    </style>
    //DialogFragment
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setStyle(android.support.v4.app.DialogFragment.STYLE_NO_TITLE, R.style.AppTheme_AppCompat_Dialog_Alert)
    }
    

    退而求其次,既然它默认设置为 wrap_content ,那么我们可以直接设置回来啊。

    //DialogFragment
    override fun onStart() {
        super.onStart()
        dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    }
    

    到这里,我们也能回答一个问题,如果 parent 指定为 wrap_content 。child 指定为 match_parent 那么最后,child 到底有多大?
    这个其实就是上面这个问题,如果要回答得简单,那么就是它就是 View 自己的 最小值。

    要详细说的话,如果 View 没有复写 onMeasure() 方法,那就是默认 onMeasure() 方法中 getDefaultSize() 的返回值,就是 pSize 。

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

    如果是其他控件,比如说刚刚说的 ProgressBar,其实就是 resolveSizeAndState() 或者测量出来的最小值。
    我们自定义 View 时视图预览发现它总会填充父布局,原因就是你没有复写 onMeasure() 方法。还有就是在写布局时,尽量避免 parent 是 wrap_content , child 又是 match_parent 的情况,这样 parent 会重复测量,造成不必要的开销。

    总结

    View 的测量是一个博弈的过程,最核心方法就是 setMeasuredDimension(),具体值则需要 parent 和 child 相互协商。数值的传递和确定依赖于 MeasureSpecLayoutParams,填充布局时 inflate() 方法 root 参数不要给空,这样会导致填充布局一些参数丢失,Dialog 总是 wrap_content ,这是因为默认带有 windowIsFloating 的属性 。

    相关文章

      网友评论

        本文标题:Android 源码分析二 View 测量

        本文链接:https://www.haomeiwen.com/subject/bvbvbqtx.html