AndroidQ FrameLayout的测量流程

上一篇文章总体分析了View的测量流程,从ViewRootImpl的performMeasure方法为入口,以递归的方式从DecoreView开始测量,这篇文章来详细分析一下FrameLayout递归测量完子View之后如何得到自己的size,比如对如下这个布局文件的测量进行分析:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

我们根据FrameLayout的测量规则来分析这个布局的测量流程

首先,我们来看看上面布局中的FrameLayout的MeasureSpec怎么生成的,上一篇文章中知道DecoreView在测量时会遍历所有子View,并调用measureChildWithMargins方法,此方法主要作用通过getChildMeasureSpec生成子View的MeasureSpec,并调用子View的measure方法,最终调用的是子View实现的onMeasure方法,将生成的MeasureSpec传递过去

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

所以FrameLayout的onMeasure方法里面接收的就是自己的MeasureSpec,我们把上一篇文章分析创建MeasureSpec的getChildMeasureSpec方法直接copy过来看看,因为DecoreView的MeasureSpec的mode为EXACTLY,size为屏幕宽高,而且上面FrameLayout的xml布局中写的宽高是wrap_content,我们这里忽略padding,margin,used,所以按照此方法的规则得到上面FrameLayout的MeasureSpec中的mode为AT_MOST,size暂定为父View的size即屏幕宽高

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //获取父View的测量模式
        int specMode = MeasureSpec.getMode(spec);
        //获取父View的size
        int specSize = MeasureSpec.getSize(spec);
        //由父View的size减去父View的padding和自己的margin以及Used
        //得到最终size(为了方便后面依然统称为父View的size)
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
        //根据父View的测量模式以及自己LayoutParams计算MeasureSpec
        switch (specMode) {
        // 如果父View的模式为EXACTLY
        case MeasureSpec.EXACTLY:
         //如果自己的xml中写的值是一个不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //则自己的size就是xml中写的值
                resultSize = childDimension;
                //自己的模式就是精确的
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中写的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //则自己的size就是父View的size
                resultSize = size;
                //自己的模式就是精确的,因为父View的模式是精确的
                resultMode = MeasureSpec.EXACTLY;
                //如果自己在xml中写的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //则自己的size暂时定为父View的size,因为此时还没有
                //对自己进行具体测量,无法得到具体值
                resultSize = size;
                //自己的模式为AT_MOST,因为就算还没有具体测量但最终
                //肯定不能超过父View的大小
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父View的测量模式为AT_MOST,即父View此时也不能确定自己的size
        case MeasureSpec.AT_MOST:
         //如果自己的xml中写的值是一个不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //则自己的size就是xml中写的值
                resultSize = childDimension;
                //自己的模式为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中写的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //则自己的size就是父View的size
                resultSize = size;
                //自己的模式为AT_MOST,因为父View都不能确定自己大小
                resultMode = MeasureSpec.AT_MOST;
                //如果自己的xml中写的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //则自己的size暂定为父View的size
                resultSize = size;
                //自己的模式为AT_MOST,自己虽然为WRAP_CONTENT,
                //但最终还是不能超多父View大小
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //如果父View模式为UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
         //如果自己的xml中写的值是一个不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //则自己的size就是xml中写的值
                resultSize = childDimension;
                //自己的模式为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中写的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //则自己的size取决与sUseZeroUnspecifiedMeasureSpec,
                //这个值在View中初始化,当sdk版本小于M时为true,大于为false
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //自己的模式为UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
                //如果自己的xml中写的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //则自己的size取决与sUseZeroUnspecifiedMeasureSpec,
                //这个值在View中初始化,当sdk版本小于M时为true,大于为false
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //自己的模式为UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //根据计算出来的size和mode生成自己的MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

FrameLayout的MeasureSpec已经计算出来了,mode为AT_MOST,size为暂定为屏幕宽高,我们就接着分析它的onMeasure方法

FrameLayout.onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取子View的个数,有两个
        int count = getChildCount();
        //如果宽或者高的测量模式有一个不是EXACTLY就为true
        //这什么意思呢?其实是为了后面对满足条件的子View进行再次测量
        //因为只要宽高有一个不是EXACTLY,就可能造成xml中有match_parent
        //的子View测量不准
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        //mMatchParentChildren里装的就是需要再次测量的子View
        //首先清空
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        //遍历所有子View
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //对需要测量所有子View的情况,或者不为Gone的view进行测量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
               //这个方法前面说过,作用是创建子View的MeasureSpec,并进行
               //测量
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                //获取子View的LayoutParams,主要是为了获取xml中写的值
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //这里取FrameLayout子View中测量出来的宽度最大值
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                //这里取FrameLayout子View中测量出来的高度最大值
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //前面说了这里为true
                if (measureMatchParentChildren) {
                    //对宽高有一个是MATCH_PARENT的子View都应该进行二次测量
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        //添加到mMatchParentChildren
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        //将子View计算出来的最大宽度再累加因为Foreground造成
        //的左右padding
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        //将子View计算出来的最大高度再累加因为Foreground造成
        //的上下padding
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        //不能小于最小高度
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        //不能小于最小宽度
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        
        final Drawable drawable = getForeground();
        //如果有背景图片
        if (drawable != null) {
           //不能小于背景图片的高
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            //不能小于背景图片的宽
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
        //还需要调用resolveSizeAndState来对计算出来的最大宽高和
        //MeasureSpec中的size进行有条件取舍,才能得到最终宽高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        //省略后面部分代码,后面一部分代码是对需要二级测量的View进行测量
        //先来分析一下resolveSizeAndState方法
        .....
           ......
    }

我们再来看看最终调用setMeasuredDimension方法之前,会调用resolveSizeAndState方法再次对最终的大小进行选择,这选择是对MeasureSpec中的size和FrameLayout子View计算出来的最大size进行选择,规则如下:

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;
        //根据FrameLayout的mode,得到对于的size
        switch (specMode) {
           //如果是AT_MOST
            case MeasureSpec.AT_MOST:
                //如果MeasureSpec中的size小于它计算出来的size
                if (specSize < size) {
                    //强制让它等于MeasureSpec中的size,因为模式已经是AT_MOST
                    //代表MeasureSpec的size是系统能给出的最大值了
                    //同时还会添加一个MEASURED_STATE_TOO_SMALL标记
                    //代表FrameLayout并没有得到想要的大小
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    //在不超过最大值的情况下肯定会让FrameLayout的大小
                    //等于计算出来的值
                    result = size;
                }
                break;
                //如果是精确模式,就等于MeasureSpec中的size
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

总结resolveSizeAndState方法规则:不考虑UNSPECIFIED情况,只有FrameLayout的mode为AT_MOST并且计算出来的size大于它MeasureSpec的最大值情况下才不会满足FrameLayout计算的值,因为系统无法提供了,只能退而求其次选择MeasureSpec中的size,而其他情况都会得到FrameLayout计算出来的值

然后调用setMeasuredDimension结束对FrameLayout的计算,FrameLayout的测量过程相对比较简单,原理就是按它里面的子View的最大宽高,再累加各种padding,并且需要满足大于最小宽高的条件得出的size,得出size之后再和MeasureSpec中的size进行选择得到最终的size

我们到现在只分析了FrameLayout的测量规则,但是并没有分析如何根据子View宽高具体得到最终的FrameLayout的宽高

我们看下这个布局,FrameLayout宽高为wrap_content,如果它里面都是写死为多少多少dp的View,那么我们根据刚刚分析的测量规则一下就知道它的大小肯定等于它里面最大的那个View宽高,但是这个布局里面有一个View宽高为match_parent,那这种情况应该怎么测量呢?

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

我们回到FrameLayout的onMeasure方法中看看它遍历子View的测量代码:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        for (int i = 0; i < count; i++) {
            ...
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ...
        }
   }

我们这里对宽高为match_parent的View的测量进行分析,measureChildWithMargins这个方法分析过多次了,它主要作用的创建子View的MeasureSpec,并调用measure进行测量

    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方法会根据父View的MeasureSpec和子View的LayoutParams计算出子View的MeasureSpec,我们忽略padding,margin,used,

父View为FrameLayout,它的moder为AT_MOST,size为屏幕宽高,所以最终对应getChildMeasureSpec方法中如下分支:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        ...
        switch (specMode) {
        ...
        case MeasureSpec.AT_MOST:
            ...
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } 
            ...
            break;
          ....
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

即父View的模式为AT_MOST,并且子View的xml中写的是MATCH_PARENT,所以最终这个子View的size暂定为父View MeasureSpec中的size,即手机屏幕宽高,模式为AT_MOST,有了MeasureSpec之后接着调用它的measure方法进行测量,因为我并没有重写View的onMeasure方法,所以最终会使用View默认的onMeasure方法:

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

默认方法其实就是简单的取MeasureSpec中的值,即这个宽高为match_parent的View的最终宽高就是屏幕的宽高,所以最终这个wrap_content的FrameLayout宽高也就是屏幕宽高

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

还没完,记得我们onMeasure方法中省略了部分代码,是对必要的View进行二次测量:什么是必要的View?就是我们前面也说过父View的模式不是EXACTLY,并且子View的宽高有一个是MATCH_PARENT的,因为针对这种情况并不能一次性得到子View的宽高,虽然前面也得到了子View的宽高,但它的模式是AT_MOST,这并不是精确的,所以需要重新计算

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        ...
        for (int i = 0; i < count; i++) {
            ...
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(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);
                    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);
            }
        }
    }

上面代码看着多,其实就是分别对宽高进行二次测量,遍历mMatchParentChildren中的View,mMatchParentChildren是存放需要二次测量的View的集合,
对里面View分两种情况重新测量,MATCH_PARENT和非MATCH_PARENT

 if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                }
                ...
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

我们看这个代码多简单,忽略padding,margin,其实就是通过getMeasuredWidth方法将View的MeasureSpec的AT_MOST模式改为EXACTLY模式,最后再调用measure重新测量,其实重新测量也是之前的大小,只是mode变为EXACTLY,说明xml中写的是一个准确的值

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_blue_dark"
>
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#652314"
        />
    <View
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ff9652"
        />
</FrameLayout>

所以最后我们可以得到宽高为match_parent的View最终测量结果为宽高等于屏幕大小,模式为EXACTLY,而最外层的FrameLayout宽高为wrap_content,根据FrameLayout测量规则得出是按最大子View算的,所以它的宽高也为屏幕大小,模式为AT_MOST,而写了具体宽高的View测量得到的结果就是xml写的,模式为EXACTLY,最终一次FrameLayout完整的测量流程就完成了,结果如下图:

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章