Android 控件-1 测量模式

Posted by xflyme on May 31, 2015

Android 的自定义控件写过很多了,但对于自定义 Layout 始终不够熟悉。原因之一是对 measure 和 layout 有点怵,测量模式在测量时占有很重要的作用。今天我们来学习下 Android的测量模式,把 ViewGroup 的神秘面纱一步步揭开。

MeasureSpec

官方文档对 MeasureSpec 的介绍是,MeasureSpec 封装了父控件对自控件的要求,每一个 MeasureSpec 代表着对宽度或者高度的要求,这个要求包含一个 size 和一种模式。

MeasureSpec 的三种模式

UNSPECIFIED

父控件对子控件没有任何要求,子控件可以要任何他想要的大小。

EXACTLY

父控件确定了严格的大小给子控件,子控件只能是这个大小。

AT_MOST

父控件给定了最大 size ,子控件不能超过它。

Demo

只说不练,可能很快就忘了,现在我们写一个标签布局练一下手。 不解释了,直接上源码:

#LabelLayout

public class LabelLayout extends ViewGroup {

    private int mItemPadding = 30;
    private int mItemSpaceing = 15;

    public LabelLayout(Context context) {
        super(context);
    }

    public LabelLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new MarginLayoutParams(lp);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //父控件传进来的宽度和高度以及对应的测量模式
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        int usedHeight = 0;
        int usedWidth = 0;

        //获取子view的个数
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);

            measureChild(child, sizeWidth, usedWidth, modeWidth, modeHeight);

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (i == 0) {
                usedHeight += childHeight;
            }

            //如果当前剩余宽度不足以放下当前这个 child 换行
            if (childWidth + usedWidth > sizeWidth) {
                usedHeight += childHeight;
                usedWidth = childWidth;
            } else {
                usedWidth = usedWidth + childWidth;
            }

        }

        int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0);
        int widthSizeAndState = resolveSizeAndState(sizeWidth, widthMeasureSpec, 0);

        setMeasuredDimension(widthSizeAndState, heightSizeAndState);

    }

    private void measureChild(View child, int parentWidth, int usedWidth, int parentSpecWidth, int parentSpecHeight) {

        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        int childWidthMeasureSpec;
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);

        switch (lp.width) {
            case MATCH_PARENT:
                //如果当前 View 的宽度是 match_parent,父 View 的测量模式是 EXACTLY 或 AT_MOST,子 View 要填满所有剩余的父空间,所以自己的测量模式是 EXACTLY
                if (parentSpecWidth == MeasureSpec.EXACTLY || parentSpecWidth == MeasureSpec.AT_MOST) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY);
                } else {
                    //父 View 的测量模式是 UNSPECIFIED,也就是说父控件的可用空间可能是无限大,自己又是 MATCH_PARENT ,那怎么填?
                    //只能把 UNSPECIFIED 传下去,而 size 只能填0
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                }
                break;
            case WRAP_CONTENT:
                //如果当前 View 的宽度是 wrap_content,父 View 的测量模式是 EXACTLY 或 AT_MOST
                // 也就是说子控件的最大可用空间是父控件的可用空间
                // 所以测量模式是 AT_MOST 而 size 是父控件的可用 size
                if (parentSpecWidth == MeasureSpec.EXACTLY || parentSpecWidth == MeasureSpec.AT_MOST) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.AT_MOST);
                } else {
                    //父 View 的测量模式是 UNSPECIFIED,也就是说父控件的可用空间可能是无限大,自己又是 WRAP_CONTENT ,那怎么填?
                    //只能把 UNSPECIFIED 传下去,而 size 只能填0
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                }
                break;
            default:
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
                break;
        }

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int width = getWidth();

        int usedWidth = 0;
        int usedHeight = 0;

        //获取子view的个数
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (i == 0) {
                usedHeight += childHeight;
            }

            if (childWidth + usedWidth > width) {
                usedHeight = usedHeight + childHeight;
                usedWidth = 0;
            }

            child.layout(usedWidth, usedHeight - childHeight, +usedWidth + +childWidth, usedHeight);

            usedWidth += childWidth;

        }


    }

}

#布局文件

<?xml version="1.0" encoding="utf-8"?>
<me.xfly.viewdragdemo.LabelLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#999999">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:text="fsasdfasfdasfdasdfasd"
        android:background="#ff0000" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:text="黑 我真的好想你"
        android:layout_margin="0dp"
        android:background="#ffff00" />

    <TextView
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#ffffff" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#ff00ff" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#00ff00" />

    <TextView
        android:layout_width="50dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#00ffff" />

    <TextView
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#0000ff" />


    <TextView
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#ff0000" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#ffff00" />

    <TextView
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#ffffff" />

    <TextView
        android:layout_width="90dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#ff00ff" />

    <TextView
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#00ff00" />

    <TextView
        android:layout_width="150dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#00ffff" />

    <TextView
        android:layout_width="80dp"
        android:layout_height="30dp"
        android:layout_margin="0dp"
        android:background="#0000ff" />


</me.xfly.viewdragdemo.LabelLayout>

效果图如下: 图1

这个 Demo 中没有考虑 margin 和 padding,不过本节主要的内容是测量模式。margin 和 padding 只是一些体力活,不影响最终效果。

总结

上面 measureChild 中注释都已经写的很清楚了,不过还是要总结一下:

  1. 在调用 child.measure 之前要先生成 child 的两个 MeasureSpec。
  2. 在生成 MeasureSpec 的时候,开发者的意见最重要,如果开发者要的是固定大小。直接给它这个大小,测量模式是 EXACTLY,MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);。
  3. 如果子 View 想要 match_parent
    • 父 View 的测量模式是 EXACTLY,子 View 的测量模式也是 EXACTLY,可用大小是父 View 剩余可用空间。 MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.EXACTLY);
    • 父 View 的测量模式是 AT_MOST,父 View 有一个最大可用空间,子 View 要占满它。MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.EXACTLY);
    • 父 View 的测量模式是 UNSPECIFIED。

      注意这里有一个问题,父 View UNSPECIFIED 也就是不限制大小,那么父 View 的可用空间就是无限大,子 View match_parent,子 View 也是无限大?size 怎么写?

    这里只能给子 View UNSPECIFIED,size 写 0。MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

  4. 如果子 View 要 wrap_content:
    • 父 View 的测量模式是 EXACTLY,子 View [最大]大小是父 View 的剩余可用空间,因此测量模式是 AT_MOST。 MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.AT_MOST);
    • 父 View 的测量模式是 AT_MOST,父 View 有一个最大可用空间,子 View 最大可用空间也是这个空间,因此测量模式是 AT_MOST。MeasureSpec.makeMeasureSpec(父 View 可用空间, MeasureSpec.EXACTLY);
    • UNSPECIFIED 同 match_parent 的 UNSPECIFIED。