ViewGroup 自定义控件 测量 布局 绘制

上一章讲了 View 的测量、布局和绘制,这一章对应的是 ViewGroup 的测量、布局和绘制,重点讲解测量,其次是布局,绘制基本为零。
ViewGroup的Measure过程,如果我们去读它的源码,发现没有 onMeasure() 方法,是不需要吗?答案是否定的,应为 ViewGroup 是个抽象类,同时也是 View 的子类,我们使用容器时,一般都是要实现各种功能的容器,所以这个测量就要根据种类来做相应的定制,比如 自适应布局的情况下, FrameLayout 是帧布局,它的大小就是子类中view最大的宽和高;
LinearLayout 是线性布局,根据垂直或水平,它的宽或高是根据子view的宽或高的相加;RelativeLayout 是相对布局,根据相对位置经历了两次计算才决定了宽和高。所以,我们自定义容器时,就要根据我们的需求,去求取对应的值,直接说的话比较笼统,以最简单的 FrameLayout 为例,分析一下。

上一章我们知道,MeasureSpec一共有三种模式,分别代表着
UPSPECIFIED : 父容器对子容器没有任何大小限制,子容器想要多大就可以由多大
EXACTLY: 父容器已经为子容器设置了尺寸,子容器需要服从这些边界, 具体的值,或者match_parent
AT_MOST:子容器可以是父容器大小内的任意大小, wrap_content

我们看看FrameLayout的源码, 简化版的 onMeasure() 方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final FrameLayout.LayoutParams lp = (FrameLayout.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());
                if (measureMatchParentChildren) {
                    if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
                            lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        ...
    }

我们先看这一段截断版的,意思就是把FrameLayout中没有gone的子view,都遍历一遍,我们注意,理解的重点放到 
    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
上,我们知道,widthMeasureSpec 是 FrameLayout 的宽,heightMeasureSpec是 FrameLayout 的高,我们点击进去,看看这个方法的代码

ViewGroup 中的源码
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    /根据父View的测量规格和父View自己的Padding、子View的Margin,及widthUsed,算出子View的MeasureSpec
        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);
    }

注意,我们看到了,在这个方法里,获取到了 View child 也就是子view的宽和高,然后调用了 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 而View
的 onMeasure(widthMeasureSpec, heightMeasureSpec) 方法就在 measure(int widthMeasureSpec, int heightMeasureSpec) 中调用,由此测量自己的宽高,看来子view的宽和高也是由上一层传进来的,我们这时就看看获取宽高的方法,看看子view的宽高是如何被父view所限制和决定的,看看 getChildMeasureSpec()方法

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;
        ...
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
方法比较长,截取第一部分,先把一种情况讲清楚,然后再继续。
第一个参数 spec 是 parentWidthMeasureSpec,也就是父View的规格;第二个参数 padding 是 父View的Padding和子View的margin及widthUsed之和;第三个参数 childDimension 是子View 的布局中设计的宽或高,即 MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); lp.width 这个值。 刚进方法,先获取父View的规格,拿到宽和Mode格式,然后会比较一下父View的宽和 第二个参数 padding 的差,如果 padding 大,说明已经没有地方给子View的空间了,size值为0,;如果父View减去padding还有剩余,那么size取这个差值,子View在这个值的范围内可以被绘制。 补充一点,MeasureSpec的模式size值,-1 代表的是EXACTLY,-2 是AT_MOST, 其他大于等于0的值代表具体值。int resultSize = 0; 为子View的宽的size初始值,int resultMode = 0; 为子View的宽的mode初始值。假设父View的mode模式为EXACTLY,我们对照上面的代码继续看,childDimension 是子View的 MeasureSpec的模式size值,大于等于0说明是具体值,此时,我们所求的子View的宽 resultSize = childDimension; 即设置的值不用改变,由于是具体的值,所以mode为resultMode =MeasureSpec.EXACTLY; 标识是具体的值。  childDimension 为 MATCH_PARENT 即为-1时,是铺满父View的剩余的地方,所以值为resultSize = size; int size = Math.max(0, specSize- padding); 由于size是固定的,所以 resultMode = MeasureSpec.EXACTLY; 即mode标识的也是具体的值。  childDimension为 WRAP_CONTENT 即-2时,意思是不超过父View,这时候就把父View剩余的地方都给它,mode值为-2,即 resultSize = size; resultMode = MeasureSpec.AT_MOST; 。

同理,只要子View的值是具体的,都是 resultSize = childDimension;  resultMode = MeasureSpec.EXACTLY;  父View为 AT_MOST 的情况下,这就表示父View自己也不确定自己的大小,所以不论子View的模式是 MATCH_PARENT 或 WRAP_CONTENT, 子View的mode都是AT_MOST,因为即使子View的mode本身是 MATCH_PARENT 铺满父View,父View根本不知道自己有多大,所以就把子View的mode强制改为了 AT_MOST ,即 resultSize = size; resultMode = MeasureSpec.AT_MOST; 。 父View为 UNSPECIFIED 的情况下,UNSPECIFIED意思是不限制大小,所以不管子View的模式是 MATCH_PARENT 或 WRAP_CONTENT, 子view的值已无意义,只要把子View的mode修改为 UNSPECIFIED即可,即 resultSize = 0;resultMode =MeasureSpec.UNSPECIFIED; 

就这样,我们在xml布局中写的view的属性,都需要经过父View的修改,最终确定其大小,我们继续看FrameLayout的onMeasure()方法,里面会比较得到其子View的宽和高,并求出最大值,然后会和FrameLayout设置的最小的宽和高及背景图的宽和高做比较,选出其中大的值,然后重点来了
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,childState << MEASURED_HEIGHT_STATE_SHIFT));
这个是设置FrameLayout的宽和高的方法,我们看看里面 resolveSizeAndState(maxWidth, widthMeasureSpec, childState) 这个方法,这个方法上一章说过,这里再重复一遍

    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是计算的最大值 maxWidth, 第二个参数measureSpec是FrameLayout的宽的值,第三个参数childMeasuredState是子View们的childState,如果不理解,可以认为是0。然后先获取FrameLayout的 specSize 和 specMode,如果FrameLayout的mode是 EXACTLY ,则置为 specSize;如果是UNSPECIFIED 不限制大小,则maxWidth,这个其实意义不大;精彩的来了,如果是AT_MOST,即包裹内容,那么我们会做一个判断,取 maxWidth 和 specSize 其中小的值作为标准,同时 如果是 specSize 小于 maxWidth,我们还会进行为运算,添加一个布局变小的标识。
假设xml根节点是FrameLayout,由于content中根节点也依附于PhoneWindow中的DecorView,DecorView的宽和高都是match_parent,所以我们如果布局

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/red">

        <View
            android:background="@color/white"
            android:layout_width="wrap_content"
            android:layout_height="100dp"/>
    </FrameLayout>

此时,上面一部分是白色,剩余部分被红色铺满;

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/main_red_day"
    android:paddingBottom="100dp"
    >

    <View
        android:background="@color/cardview_dark_background"
        android:layout_width="wrap_content"
        android:layout_height="100dp"/>

</FrameLayout>
这时候会发现,上面是灰色,中间是红色,下面是白色。为什么呢?灰色是子View的颜色,红色是FrameLayout的颜色,因为android:paddingBottom="100dp",下面的白色实际上是DecorView它的颜色,由于FrameLayout的格式是wrap_content,再加上子View的高度+FrameLayout的padding值没有超过屏幕,所以就是这样了。这样的计算模式正是印证了resolveSizeAndState()方法中的判断。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/main_red_day"
    >
    
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/food_ratingbar"
        />
</FrameLayout>

food_ratingbar 是个星星的图片,这时候会正常显示,只有星星的背景也就是FrameLayout是红色的,其他都是白色。这时候还算正常,但是,如果背景图换成颜色的背景图,那么

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/main_red_day"
    >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/dark" // 灰色
        />
</FrameLayout>

这时候,ImageView的宽和高都换成了wrap_content, 整体都是白色,根本就没灰色和红色,

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/main_red_day"
    >

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/dark" // 灰色
        />
</FrameLayout>

ImageView的宽和高都换成了 match_parent ,还是没有红色和灰色。什么呢?原因在于ImageView 的 onMeasure() 方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        resolveUri();
        ...
        // We are allowed to change the view's width
        boolean resizeWidth = false;
        boolean resizeHeight = false;

        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (mDrawable == null) {
            // If no drawable, its intrinsic size is 0.
            mDrawableWidth = -1;
            mDrawableHeight = -1;
            w = h = 0;
        } else {
            w = mDrawableWidth;
            h = mDrawableHeight;
            if (w <= 0) w = 1;
            if (h <= 0) h = 1;
            if (mAdjustViewBounds) {
                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;

                desiredAspect = (float) w / (float) h;
            }
        }
        if (resizeWidth || resizeHeight) {
            ...
        } else {
            // 走到这里 *****
            w += pleft + pright;
            h += ptop + pbottom;

            w = Math.max(w, getSuggestedMinimumWidth());
            h = Math.max(h, getSuggestedMinimumHeight());

            widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
            heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
        }
        setMeasuredDimension(widthSize, heightSize);
    }

按照我们的写法,先执行resolveUri();方法,最终到 updateDrawable(Drawable d) 方法,获取到 mDrawableWidth = d.getIntrinsicWidth(); mDrawable 为 color 的drawable,ColorDrawable 中没有复写这个方法,父类中值为-1;继续往 下看,mDrawable 不为 null,则第一个if中,走到了else中,此时 w 和 h 均为1,,mAdjustViewBounds默认值为false,所以最后一个else的地方:走到这里*****, 然后 pleft  pright  ptop  pbottom 四个值是 padding,都为0,此时,w 和 h 还是1,然后进入了最后计算阶段,resolveSizeAndState()方法,我们看看它的源码这不就是刚才说的那个方法吗?经过一番对比取值,很明显,最终 resolveSizeAndState(w, widthMeasureSpec, 0) 方法中,w的值为1,widthMeasureSpec 的值为屏幕的宽,假设为1080*1920的手机,那么widthMeasureSpec值为1080,此时 resolveSizeAndState()方法中的 imageView 的 mode 为 AT_MOST, 此时,取最小的值,即 1,也就是说,widthSize = 1,同理 heightSize = 1,然后调用 setMeasuredDimension(widthSize, heightSize); 方法,imageView 的宽高都是1px,差一点的手机根本就显示不出来,由于FrameLayout 也是包裹内容,所以FrameLayout的宽高也都是1px,所以填充color时什么也没显示。 同理,imageView 填充星星图片时,由于Drawable图片有具体的宽和高,所以就显示出来了。

下面看一下 layout 的过程,ViewGroup这个onLayout()方法是抽象的,需要子类去实现,重写实现布局。还是以FrameLayout为例,我们在onMeasure中计算出了子View的宽和高,在onLayout()方法中,我们先减去FrameLayout的本身的padding值,计算出可以布局子View的区域,然后根据子View的宽和高,及 Gravity 属性,来计算出每个view应该所在的位置,计算出每一个View的左上角的坐标,距离父View也就是FrameLayout的左边和上面的距离,有了左上角的坐标,图片本身的宽和高,那么就计算出图片由下角的坐标,有了这四个坐标,直接调用 View 的 layout() 方法,系统就开始布局位置了。

最后一个 onDraw() 方法,ViewGroup 一般用不到,ViewGroup 的子类属于容器级别,一般都是用来装view的,很少自己绘制东西的,如果有特殊需求,可以像在View中使用onDraw() 方法一样,去使用。

不管是View 还是 ViewGroup, onMeasure() 都是重头戏。绘制宽高理解了,剩下两个就好理解了。

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/88703705
今日推荐