美文网首页自定义控件Android自定义View
自定义控件中,measure的流程

自定义控件中,measure的流程

作者: JamFF | 来源:发表于2019-03-26 11:10 被阅读17次

    继承 View 的子类

    一般来说继承 View 的子类需要重写 onMeasure() ,会在 measure() 中被调用,而 measure() 是被 final 修饰的,也就表明它不希望被重写,所以只要重写 onMeasure() 完成测量即可。

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

    onMeasure() 只有一行代码,进入 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;
    }
    

    可以发现,这里面的 AT_MOST 和 EXACTLY 两种模式,得到的 size 都是一样。也就是说,不论 View 设置 wrap_content 还是 match_parent,getDefaultSize() 都会返回父容器剩余的空间。所以,在自定义 View 的时候,如果不重写 onMeasure(),设置宽高为 wrap_content 或 match_parent 时,展示是没有任何区别的。

    下面我们先看下熟悉的 TextView 的 onMeasure() 的源码。

    TextView 源码分析

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
        int width;
        int height;
        ...省略代码...
        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;// 当前view的尺寸就为父容器的尺寸
        } else {
            ...省略代码...
            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);// 当前view的尺寸就为内容尺寸和父容器尺寸当中的最小值
            }
        }
        ...省略代码...
        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;// 当前view的尺寸就为父容器的尺寸
            mDesiredHeightAtMeasure = -1;
        } else {
            ...省略代码...
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);// 当前view的尺寸就为内容尺寸和父容器尺寸当中的最小值
            }
        }
        ...省略代码...
        setMeasuredDimension(width, height);// 调用View的方法
    }
    

    最后调用 View.java 的 setMeasuredDimension() 保存 measuredWidth 和 measuredHeight。

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
    
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        // 保存宽高,注意是measuredWidthm不是width
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    

    套路总结

    View 的 measure 的流程就是 measure -> onMeasure -> setMeasuredDimension -> setMeasuredDimensionRaw

    在自定义 View 只需要重写 onMeasure() 测量自己的宽高,最终调用 setMeasuredDimension() 保存自己的测量宽高。
    伪代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        
        int viewSize = 0;
        switch (mode) {
            case MeasureSpec.EXACTLY:
                viewSize = size;//当前view的尺寸就为父容器的尺寸
                break;
            case MeasureSpec.AT_MOST:
                viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。
                break;
            case MeasureSpec.UNSPECIFIED:
                viewSize = getContentSize();//内容有多大,久设置多大尺寸。
                break;
            default:
                break;
        }
        setMeasuredDimension(viewSize);
    }
    

    继承 ViewGroup 的子类

    和 View 一样,只需要重写 onMeasure() 即可,但是里面涉及到 child 的测量,还是比较复杂的。

    FrameLayout 源码分析

    这里目的是总结归纳,所以简化并修改了一些代码,源码要比下面复杂的多。
    FrameLayout 中的 onMeasure() 方法。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            ...省略代码...
            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);
        }
        ...省略代码...
        // ⑤ 保存 FrameLayout 的 measuredWidth 和 measuredHeight
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        ...省略代码...
    }
    

    ViewGroup 的 measureChildWithMargins() 方法。

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        // ② 为每一个 child 计算 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 完成测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    View 的 resolveSizeAndState() 方法。

    // ④ 计算 FrameLayout 的 measuredWidth 和 measuredHeight
    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);
    }
    

    上面主要的流程是

    1. ②处 为每一个 child 计算 MeasureSpec。
    2. ③处 对 child 完成测量。
    3. ④处 计算 FrameLayout 的 measuredWidth 和 measuredHeight。
    4. ⑤处 保存 FrameLayout 的 measuredWidth 和 measuredHeight。

    LinearLayout 源码分析

    这里目的是总结归纳,所以简化并修改了一些代码,源码要比下面复杂的多。
    LinearLayout 有 HORIZONTAL 和 VERTICAL 两种样式,这里就用纵向举例 measureVertical() 方法。

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    
        int maxWidth = 0;
        int childState = 0;
    
        final int count = getVirtualChildCount();
    
        ...省略代码...
    
        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
        ...省略代码...
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);// ①
        ...省略代码...
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
        }
    
        maxWidth += mPaddingLeft + mPaddingRight;
    
        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);// ⑥ 保存 LinearLayout 的 measuredWidth 和 measuredHeight。
    }
    

    LinearLayout 的 measureChildBeforeLayout() 方法。

    void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
      measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);// ②
    }
    

    ViewGroup 的 measureChildWithMargins() 方法。

    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);// ③ 为每一个 child 计算 MeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);// ③ 为每一个 child 计算 MeasureSpec
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);// ④ 对child 完成测量
    }
    

    View 的 resolveSizeAndState() 方法。

    // ⑤ 计算 LinearLayout 的 measuredWidth 和 measuredHeight
    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);
    }
    

    上面的流程是

    1. ③处 为每一个 child 计算 MeasureSpec。
    2. ④处 对 child 完成测量。
    3. ⑤处 计算 LinearLayout 的 measuredWidth 和 measuredHeight。
    4. ⑥处 保存 LinearLayout 的 measuredWidth 和 measuredHeight。

    套路总结

    ViewGroup 的 measure 的流程就是 measure -> onMeasure(测量子控件的宽高) -> setMeasuredDimension -> setMeasuredDimensionRaw(保存自己宽高)。
    通过 FrameLayout 和 LinearLayout 不难看出 自定义 ViewGroup 的 measure 的流程。主要是两点:

    1. 测量所有子控件的尺寸。
    2. 设置自己的尺寸。

    伪代码:

    // 为每一个child计算测量规格信息(MeasureSpec)
    getChildMeasureSpec();
    // 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
    child.measure();
    // 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
    child.getChildMeasuredSize();//child.getMeasuredWidth() 和 child.getMeasuredHeight()
    // ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
    ViewGroup.calculateSelfSize();
    // 保存ViewGroup自己的尺寸
    setMeasuredDimension(size);
    

    自定义 ViewGroup 的实现

    最后写个 demo,按照上面总结的套路,自定义 ViewGroup ,实现下面的效果。


    自定义 ViewGroup

    @UiThread
    public class MyViewGroup extends ViewGroup {
    
        private static final int OFFSET = 80; // 每个child横向偏移量
    
        public MyViewGroup(Context context) {
            this(context, null);
        }
    
        public MyViewGroup(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            int width = 0;
            int height = 0;
    
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                LayoutParams lp = child.getLayoutParams();
                // 为每一个child计算测量规格信息(MeasureSpec)
                int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
                int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
                // 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
                child.measure(childWidthSpec, childHeightSpec);
            }
    
            // ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
            switch (widthMode) {
                case MeasureSpec.EXACTLY:
                    width = widthSize;
                    break;
                case MeasureSpec.AT_MOST:
                case MeasureSpec.UNSPECIFIED:
                    for (int i = 0; i < childCount; i++) {
                        View child = getChildAt(i);
                        // 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
                        int widthAndOffset = i * OFFSET + child.getMeasuredWidth();
                        width = Math.max(width, widthAndOffset);
                    }
                    break;
                default:
                    break;
            }
    
    
            switch (heightMode) {
                case MeasureSpec.EXACTLY:
                    height = heightSize;
                    break;
                case MeasureSpec.AT_MOST:
                case MeasureSpec.UNSPECIFIED:
                    for (int i = 0; i < childCount; i++) {
                        View child = getChildAt(i);
                        // 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
                        height = height + child.getMeasuredHeight();
                    }
                    break;
                default:
                    break;
            }
            // 保存ViewGroup自己的尺寸
            setMeasuredDimension(width, height);
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // 摆放
            int left = 0;
            int top = 0;
            int right = 0;
            int bottom = 0;
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                left = i * OFFSET;
                right = left + child.getMeasuredWidth();
                bottom = top + child.getMeasuredHeight();
                child.layout(left, top, right, bottom);
    
                top += child.getMeasuredHeight();
            }
        }
    }
    

    布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <com.ff.ui.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="我是文本aaaa" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="我是文本bbbb" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="我是文本cccc" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="我是文本ddddd" />
    
    </com.ff.ui.MyViewGroup>
    

    相关文章

      网友评论

        本文标题:自定义控件中,measure的流程

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