Android 自定义View学习(十一)——ViewGroup测量知识学习

学习资料

  • Android开发艺术探索
  • 鸿洋大神的Android 手把手教您自定义ViewGroup(一)
  • 爱哥自定义控件其实很简单7/12

上篇学习了View的测量方法,了解一些Android UI架构图的知识,这篇记录学习ViewGroup的测量


1. ViewGroup

A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.

直译: ViewGroup是一个可以包含其他子View特殊的View。并且是那些子View或者布局的父容器。而且ViewGroup定义了ViewGroup.LayoutParams这个类

ViewGroup是一个抽象类,内部的子View可以是一个View也可以是另一个ViewGroup

例如,在LinearLayout中,可以加入一个TextView也可以加入另外一个LinearLayout


ViewGroup的职责

ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。

View的职责

View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。

以上摘抄鸿洋大神的Android 手把手教您自定义ViewGroup(一)


2. 测量方法

View的测量大小除了自身还会受父容器的影响。一般这个父容器就是一个ViewGroup。对于一个ViewGroup来说,除了完成自身的测量外,还要遍历内部的childView的测量方法,各个childView再递归执行这个步骤。

ViewGroup源代码内并没有重写onMeasure()方法,而是提供了几个测量相关的方法。

原因也比较容易理解,由于ViewGroup是一个抽象类,有不同的子类childView,有不同的布局属性,测量的细节不同。例如LinearLayputRelativeLayout。每个继承之ViewGroupLayout,各自根据自身的布局属性来重写onMeasure()方法


2.1 测量的过程

ViewGroup的测量过程主要用到了三个方法

  1. measureChildren() ,遍历所有的childView
  2. getChildMeasureSpec(),确定测量规格
  3. measureChild(),调用测量规格。这个方法内,根据2确定好的测量规格,childView调用了measure()方法,而measure()内部调用的方法就有onMeasure()

2.1.1 measureChildren() 遍历所有的childView

源码:

/**
 * Ask all of the children of this view to measure themselves, taking into account both the MeasureSpec requirements for this view and its padding.
 *
 * We skip children that are in the GONE state The heavy liftingis done in getChildMeasureSpec.
 *
 * @param widthMeasureSpec The width requirements for this view
 * @param heightMeasureSpec The height requirements for this view
 */
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) {//确定childview是否可见
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

方法内主要就是遍历了所有的chlidView,判断每个childViewvisibility值,确定当前的这个childView可见,然后调用了measureChild(child, widthMeasureSpec, heightMeasureSpec)方法


2.1.2 measureChild(),调用测量规格

把这个方法放在getChildMeasureSpec()确定测量规格之前,是因为measureChild()内部调用了getChildMeasureSpec()

源码:

    /**
     * Ask one of the children of this view to measure itself, taking into account both the MeasureSpec requirements for this view and its padding.
     *
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
        // 获取childView的布局参数
        final LayoutParams lp = child.getLayoutParams();

        //将ViewGroup的测量规格,上下和左右的边距还有childView自身的宽高传入getChildMeasureSpec方法计算最终测量规格 
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);

        //调用childView的measure(),measure()方法内就是回调`onMeasure()`方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

Viewmeasure()测量方法调用过程,在上篇View的测量方法学习过程中,只是用文字简单概括了几句,并没有记录学习源码方法的调用过程,可以去爱哥的自定义控件其实很简单7/12进行补充学习 : )


2.1.3 getChildMeasureSpec(),确定childview的测量规格

源码:

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to pass to a particular child. This method figures out the right MeasureSpec for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the LayoutParams of the child to get the best possible results. For example, if the this view knows its size (because its MeasureSpec has a mode of EXACTLY), and the child has indicated in its LayoutParams that it wants to be the same size as the parent, the parent should ask the child to layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and margins, if applicable
     * @param childDimension How big the child wants to be in the current dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //ViewGroup的测量模式及大小
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //将ViewGroup的测量大小减去内边距
        int size = Math.max(0, specSize - padding);

        // 声明临时变量存值  
        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        case MeasureSpec.EXACTLY://ViewGroup的测量模式为精确模式
            //根据childView的布局参数判断 
            if (childDimension >= 0) {//如果childDimension是一个具体的值  
                // 将childDimension赋予resultSize ,作为结果
                resultSize = childDimension;
                //将临时resultMode 也设置为精确模式
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//childView的布局参数为精确模式  
               //将ViewGroup的大小做为结果
                resultSize = size;
                //因为ViewGroup的大小是受到限制值的限制所以childView的大小也应该受到父容器的限制  
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//childView的布局参数为最大值模式 
                //ViewGroup的大小作为结果  
                resultSize = size;
              //将临时resultMode 也设置为最大值模式
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST://ViewGroup的测量模式为精确模式
            //根据childView的布局参数判断 
            if (childDimension >= 0) {//如果childDimension是一个具体的值  
                 // 将childDimension赋予resultSize ,作为结果
                resultSize = childDimension;
                //将临时resultMode 也设置为精确模式
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//如果childDimension是精确模式 
                //因为ViewGroup的大小是受到限制值的限制所以chidlView的大小也应该受到父容器的限制 
                 //ViewGroup的大小作为结果  
                resultSize = size;
                 //将临时resultMode 也设置为最大值模式
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 如果childDimension是最大值模式 
                
                 //ViewGroup的大小作为结果  
                resultSize = size;
                 //将临时resultMode 也设置为最大值模式
                //childView的大小包裹了其内容后不能超过ViewgGroup               
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.UNSPECIFIED://ViewGroup尺寸大小未受限制  
            if (childDimension >= 0) {//如果childDimension是一个具体的值  
                 // 将childDimension赋予resultSize ,作为结果
                resultSize = childDimension;
                 // 将临时resultMode 也设置为精确模式 
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {如果childDimension是精确模式
               //ViewGroup大小不受限制,对childView来说也可以是任意大小,所以不指定也不限制childView的大小
               //对是否总是返回0进行判断 sUseZeroUnspecifiedMeasureSpec受版本影响
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                 // 将临时resultMode 也设置为UNSPECIFIED,无限制摸式 
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {如果childDimension是最大值
                //ViewGroup大小不受限制,对childView来说也可以是任意大小,所以不指定也不限制childView的大小
                //sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                 // 将临时resultMode 也设置为UNSPECIFIED,无限制摸式 
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //返回封装后的测量规格  
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

至此我们可以看到一个View的大小由其父容器的测量规格MeasureSpecView本身的布局参数LayoutParams共同决定,但是即便如此,最终封装的测量规格也是一个期望值,究竟有多大还是我们调用setMeasuredDimension方法设置的。上面的代码中有些朋友看了可能会有疑问为什么childDimension >= 0就表示一个确切值呢?原因很简单,因为在LayoutParams中MATCH_PARENTWRAP_CONTENT均为负数、哈哈!!正是基于这点,Android巧妙地将实际值和相对的布局参数分离开来。

以上摘自爱哥的自定义控件其实很简单7/12


3. 布局方法

ViewGorup是个抽象类,继承ViewGroup,肯定就有必须要实现的抽象方法,这个抽象方法就是onLayout()

代码:

public class CustomLayout extends ViewGroup {
    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    }
}

经过了onMeasure()方法后,确定ViewGroup的位置和childView宽高后,在ViewGrouponLayout()方法内,遍历ViewGroup内所有的childView,并让每个childView调用Viewlayout()方法,在layout()方法内,首先会确定每个childView的顶点的位置,之后又调用childViewonLayout()方法


3.1 简单实现CustomLayout

public class CustomLayout extends ViewGroup {
    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int count = getChildCount();
        if (count > 0) {
            measureChildren(widthMeasureSpec, heightMeasureSpec);
        }
    }

    /**
     * 布局
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        if (count > 0) {
            // 遍历内部的childView
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);                                    
                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
            }
        }
    }
}

代码很简单,就是先遍历测量,在遍历布局


布局xml:



    

    
Android 自定义View学习(十一)——ViewGroup测量知识学习_第1张图片
CustomLayout

虽然TextViewButtonCustomLayout都已经绘制出来,但ButtonTextView给盖住了。原因很明显,在绘制第2个子控件Button时,依然从CustomView(0,0)点开始绘制,并没有考虑TextView的高度


3.2 进行优化修改

修改需要考虑的就是已经绘制过的childView的高度

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    if (count > 0) {
        int mHeight = 0;
        // 遍历内部的childView
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            child.layout(0, mHeight, child.getMeasuredWidth(), child.getMeasuredHeight()+mHeight);
            mHeight +=  child.getMeasuredHeight();
        }
    }
}

增加一个临时变量int mHeight = 0,绘制过TextView就将高度加起来,就等于绘制Button时,开始绘制的点便是(0,mHeight),于是,Button也就在TextView下方

Android 自定义View学习(十一)——ViewGroup测量知识学习_第2张图片
考虑已经绘制过的childView的高

有点像一个超级简单的VerticalLinearLayout

Horizontal的,就可以考虑child.layout()时,改变开始绘制时,x轴的坐标点


3.3 getMeasuredWidth()和getWidth()

onLayout()方法中

child.layout(0, 0, child.getMeasuredWidth(),child.getMeasuredHeight())

使用的是child.getMeasuredWidth(),而不是child.getWidth()


child.getWidth()源码:

/**
 * Return the width of the your view.
 *
 * @return The width of your view, in pixels.
 */
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
    return mRight - mLeft;
}

其中mRightmleft值,是在onLayout()方法后拿到的,在onLayout()方法中,返回的是0


child.getMeasuredWidth()源码:

    /**
     * Like {@link #getMeasuredWidthAndState()}, but only returns the raw width component (that is the result is masked by {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured width of this view.
     */
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

之后便是追着mMeasuredWidth这个值走,经过一系列的测量方法后,最终来到onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(...);
}

mMeasuredWidth则在onMeasure()方法后便可以到了,拿到的时间比getWidth()要早


使用场景:

  • getMeasuredWidth():onLayout()方法内
  • getWidth():除了onLayout()方法,其他之外

使用场景绝大部分情况下都是符合的,这两个方法拿到的值,绝大多数时候也是一样的

可以看看Android开发之getMeasuredWidth和getWidth区别从源码分析


4.考虑Padding,Margins

有了上篇onMeasure()经验,知道PaddingMargins,也需要优化处理的


4.1 Padding

xml文件中加入padding之后

Android 自定义View学习(十一)——ViewGroup测量知识学习_第3张图片
Padding将内容吃掉

修改代码:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
   final int count = getChildCount();
   final int parentPaddingLeft = getPaddingLeft();
   final int parentPaddingTop = getPaddingTop();
   if (count > 0) {
       int mHeight = 0;
       // 遍历内部的childView
       for (int i = 0; i < count; i++) {
           View child = getChildAt(i);
           final int left = parentPaddingLeft;
           final int top = mHeight + parentPaddingTop;
           final int right = child.getMeasuredWidth() + parentPaddingLeft;
           final int bottom = child.getMeasuredHeight() + mHeight + parentPaddingTop;
    child.layout(left, top, right, bottom);
            mHeight += child.getMeasuredHeight();
        }
    }
}

主要就是考虑getPaddingLeft()getPaddingTop()

Android 自定义View学习(十一)——ViewGroup测量知识学习_第4张图片
简单优化Padding

这样也只是做了最简单的优化,一旦Padding大到了一定程度,还是会吃掉内部的childView


4.2 Margins

CustomLayout内加Margins有效,可内部的childView加了却无效。上篇提到过,ViewMargins是封装在LayoutParams后由ViewGroup来处理的

自定义LayoutParams:

public static class LayoutParams extends MarginLayoutParams {

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}

并没有做任何设置,还对更多属性进行设置,以后再学习


完整代码:

public class CustomLayout extends ViewGroup {


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

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int count = getChildCount();
        // 临时ViewGroup大小值
        int viewGroupWidth = 0;
        int viewGroupHeight = 0;
        if (count > 0) {
            // 遍历childView
            for (int i = 0; i < count; i++) {
                // childView
                View child = getChildAt(i);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //测量childView包含外边距
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                // 计算父容器的期望值
                viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
                viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }

            // ViewGroup内边距
            viewGroupWidth += getPaddingLeft() + getPaddingRight();
            viewGroupHeight += getPaddingTop() + getPaddingBottom();

            //和建议最小值进行比较
            viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
            viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
        }
        setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
    }


    /**
     * 布局
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // ViewGroup的内边距
        int parentPaddingLeft = getPaddingLeft();
        int parentPaddingTop = getPaddingTop();
        if (getChildCount() > 0) {
            int mHeight = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                //获取 LayoutParams
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //childView的四个顶点
                final int left = parentPaddingLeft + lp.leftMargin;
                final int top = mHeight + parentPaddingTop + lp.topMargin;
                final int right = child.getMeasuredWidth() + parentPaddingLeft + lp.leftMargin;
                final int bottom = child.getMeasuredHeight() + mHeight + parentPaddingTop + lp.topMargin;

                child.layout(left, top, right, bottom);
                // 累加已经绘制的childView的高
                mHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }
        }
    }

    /**
     *  获取布局文件中的布局参数
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CustomLayout.LayoutParams(getContext(), attrs);
    }

    /**
     *  获取默认的布局参数
     */
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    /**
     *  生成自己的布局参数
     */
    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    /**
     *  检查当前布局参数是否是我们定义的类型
     */
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    /**
     * 自定义LayoutParams
     */
    public static class LayoutParams extends MarginLayoutParams {

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

代码基本照搬的爱哥的。。。。


xml布局文件



    

    
Android 自定义View学习(十一)——ViewGroup测量知识学习_第5张图片
支持Margin

这时,CustomLayout和内部控件的Margin都已经支持,但真正以后实际开发,要优化考虑的要比这严谨。这里只是了解学习


5.最后

重点是理解ViewGroup的测量过程,理解后,接下来再学习View的工作流程就会比较容易理解

本人很菜,有错误,请指出

共勉 : )

你可能感兴趣的:(Android 自定义View学习(十一)——ViewGroup测量知识学习)