美文网首页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