美文网首页Android
Android控件测量-MesaureSpec详解

Android控件测量-MesaureSpec详解

作者: Timmy_zzh | 来源:发表于2021-02-27 20:23 被阅读0次

1.View的测量

  • 如果我们想要在界面上画一个图形,那么必须要先知道控件的大小和位置。所以系统在绘制View之前,必须要先对View进行测量,这样才能知道要绘制一个多大的View。这个过程在onMeasure()方法中进行。
MeasureSpec
  • MeasureSpec类是系统提供的,该类封装了View测量使用到的数据,包括测量模式和测量大小

    • 他使用一个32位的int值,其中高2位表示测量的模式,低30位表示测量的大小。
  • 测量模式分为3种

    • EXACTLY:表示在xml布局文件中宽高使用match_parent或者固定大小的宽高
    • AT_MOST:表示在xml布局文件中宽高使用wrap_content
    • UNSPECIFIED:父容器没有对当前View有任何限制,当前View可以取人意尺寸,比如ListView中的item
  • View类默认的onMeasure方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure方法的话,就只能使用EXACTLY模式。

    • 控件可以响应xml布局中设置具体的宽高值或者match_parent属性。
    • 而如果要让自定义View支持wrap_content属性,就必须要重写onMeasure方法来指定wrap_content的大小
  • View的默认onMeasure方法

public class View implements ... {
  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    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;
    }
  
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight,     mBackground.getMinimumHeight());
    }
}

2.ViewGroup的测量

  • ViewGroup需要管理其子View,包括子View的显示大小。
    • 当ViewGroup的大小设置为match_parent或固定dp值时,可以通过具体的指定值来设置ViewGroup自身的大小
    • 当ViewGroup的大小为wrap_content,ViewGroup就需要对子View进行遍历,从而来决定自己的大小。
  • ViewGroup在测量时会通过遍历所有子View,从而调用子View的measure方法来获得每个子view的测量结果,View的测量逻辑就是在这里进行的。
ViewGroup的onMeasure
  • 在ViewGroup测量方法中需要对子view进行测量,可以通过如下方法实现:
    • measureChild
    • measureChildren
    • measureChildWithMargins
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
  
        protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

        protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}
  • 其中measureChildren接着会调用measureChild方法,最终都会调用子view的measure方法,并传入参数childWidthMeasureSpec和childHeightMeasureSpec,那这两个值是如何得到的呢?
    • 其是通过父布局的MeasureSpec和自身的布局参数LayoutParams共同决定,在ViewGroup的getChildMeasureSpec方法中实现并返回。

其中LayoutParams源码如下:

    public static class LayoutParams {
        @Deprecated
        public static final int FILL_PARENT = -1;
        public static final int MATCH_PARENT = -1;
        public static final int WRAP_CONTENT = -2;
        
        public int width;
        public int height;
 
        public LayoutParams(int width, int height) {
            this.width = width;
            this.height = height;
        }
      
        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }
   }
  • 解析:所以LayoutParams的宽高值有两种来源
    • 通过new LayoutParams() 设置
    • 通过xml不仅设置
    • 且取值只有三中情况:
      • -2(WRAP_CONTENT)
      • -1 (MATCH_PARENT)
      • 固定大小相素值
getChildMeasureSpec
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    //参数一:spec 为父控件的MeasureSpec
    //参数二:padding 父空间已消耗的大小
    //参数三:childDimension 子View的LayoutParams中的宽高值
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      
        //父控件的测量模式和大小
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //size为留给子view剩余的最大空间
        int size = Math.max(0, specSize - padding);

        //最终需要计算确定的子view的大小和模式,并组合成MeasureSpec返回
        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // 父控件测量模式是 EXACTLY,表示父控件大小为固定值
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {          
                    //表示子view的大小设置为固定值--》则传递给子类的大小为固定值和EXACTLY模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 表示子view大小设置为MATCH_PARENT,子view使用父控件剩余的可用空间
                //--》传递给子view的大小为固定值就是父控件的剩余可用空间和EXACTLY模式
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 表示子view的大小设置为WRAP_CONTENT包裹类型,
                // --》传递给子view的大小为父控件的剩余可用空间和AT_MOST模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父控件测量模式是 AT_MOST,表示父控件大小为包裹大小
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //表示子view的大小设置为固定值--》则传递给子类的大小为子view的固定值和EXACTLY模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 表示子view大小设置为MATCH_PARENT,子view想使用父控件剩余的可用空间,
                // 但是父控件大小不固定,所以子控件也是固定不了,模式也转换成AT_MOST包裹类型
                //--》传递给子view的大小为固定值就是父控件的剩余可用空间和 AT_MOST 模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 父控件和子控件都使用WRAP_CONTENT 包裹模式
                // 传递给子view的大小为父控件的剩余可用空间和 AT_MOST 模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
        // 多见于ListView、GridView  
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // 子view大小为子自身所赋的值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 因为父view为UNSPECIFIED,所以MATCH_PARENT的情况下,子类大小为0 
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 因为父view为UNSPECIFIED,所以 WRAP_CONTENT 的情况下,子类大小为0 
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
      
        //组装
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}
  • 归纳整理如下:
子view布局参数(LayoutParams)\ 父控件测量模式 EXACTLY AT_MOST UNSPECIFIED
具体数值(dp/px) EXACTLY + childSize EXACTLY + childSize EXACTLY + childSize
match_parent EXACTLY + parentSize(父容器剩余空间) AT_MOST + parentSize(大小不能超过父容器的剩余空间) UNSPECIFIED + 0
wrap_content AT_MOST + parentSize(大小不能超过父容器的剩余空间) AT_MOST + parentSize(大小不能超过父容器的剩余空间) UNSPECIFIED + 0
规律总结
  • 当子view采用具体数值(dp/px)时,子View的MeasureSpec的值为:

    • 测量模式 = EXACTLY
    • 测量大小 = 子view自身设置的具体大小值
  • 当子view采用match_parent时,子View的MeasureSpec的值为:

    • 测量模式 = 父容器的测量模式
    • 测量大小:
      • 如果父容器的测量模式为EXACTLY,那么测量大小 = 父容器的剩余空间
      • 如果父容器的测量模式为AT_MOST,那么测量大小 = 不超过父容器的剩余空间
  • 当子view采用wrap_contentt时,子View的MeasureSpec的值为:

    • 测量模式 = AT_MOST
    • 测量大小:不超过父容器的剩余空间

相关文章

网友评论

    本文标题:Android控件测量-MesaureSpec详解

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