自定义ViewGroup

Posted Android_Study_OK

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义ViewGroup相关的知识,希望对你有一定的参考价值。

引子:标准的自定义ViewGroup应该包含什么

第一部分

  1. 支持wrap_content

    即当ViewGroup的宽、高使用wrap-content时,ViewGroup的高宽根据子View的实际大小来确定

    如果你不处理的话,“wrap-content”的和 “match-parent”是一样的

  2. ViewGroup支持Padding

  3. 其子View支持margin

  4. 支持自定义属性

    例如:gravity

第二部分

  1. 支持滚动效果(当然,是你有滚动的需求的话)

第三部分

  1. 滑动事件冲突处理

    touch事件分发

接下来,我们一步一步实现以上内容

第一步、自己来写一个横向的LinearLayout(先实现支持wrap_content和padding)

必须要掌握的内容

  1. onMeasure的写法

  2. onLayout的写法

难点

  1. onMeasure中widthMeasureSpec、heightMeasureSpec的理解

    这里不具体解释这个问题,只需知道,就是因为MeasureSpec的规则导致了,如果你不进行特殊处理,致使wrap-content的效果为match-parent

    展开说一句:儿子的尺寸,不完全由儿子自己决定,与父亲传递过来的测量规格是有关系的

  2. onLayout中l,t,r,b的含义

    解释一下:int l, int t, int r, int b,这四个值

    这四个值,就是通过onMeasure方法测量后,得到的这个MyViewGroup的左上右下值

    注意:l,t,r,b是考虑了其所在的父ViewGroup的padding值的(MyviewGroup自身的padding值,当然也考虑进去了)

    这四个值,大家可以打印出来看看就明白了,这个不清楚的话,很难写好onLayout方法

    l=自定义ViewGroup的父ViewGroup的leftPadding

    t=自定义ViewGroup的父ViewGroup的topPadding

    r=自定义ViewGroup的父ViewGroup的leftPadding+这个MyViewGroup的宽度(即上文的desireWidth)

    t=自定义ViewGroup的父ViewGroup的topPadding+这个MyViewGroup的高度(即上文的desireHeight)

开始

大家都知道,自定义View要经过onMeasure、onLayout、onDraw三个流程

onMeasure里要完成的内容(再次强调这里说的是自定义ViewGroup,自定义View不是本文讨论的范畴)

  1. 要测量其内部所有的子View的宽高

    measureChild(View child, int parentWidthMeasureSpec,
    int parentHeightMeasureSpec)

    measure(int widthMeasureSpec, int heightMeasureSpec)

    上面两个方法都可以使用,意思是一样的

    • 第一个是,测量每一个子View的方法
    • 第二个是,让每一个子View去测量自己的方法
  2. 测量ViewGroup自己的宽高

    • 你要是懒得支持wrap_content,只需要写

      super.onMeasure(widthMeasureSpec,heightMeasureSpec);即可,

      会调用父类的流程去测量你自定义的ViewGroup的宽高(不具体展开源码,源码其实调用的是setMeasuredDimension(int measuredWidth, int measuredHeight)

      不支持wrap-content的意思就说:当你设置宽高为wrap-content时候,实际的效果却是match_content

    • 你要是支持wrap_content的话,
      使用下面的方法来测量ViewGroup自身的宽高

      setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
      resolveSize(desireHeight, heightMeasureSpec));

      你肯定想当你的ViewGroup宽度为wrap-content时,这个ViewGroup的宽度值是所有子View的宽度之和再加上左右padding(先不考虑子View的margin),

      这就需要你在测量每一个子View的宽度时,累加所有的子View宽度再加上左右padding值作为ViewGroup的宽度(当ViewGroup的宽度值你设置的是wrap_content时,你要是设100dp,那就直接是100dp了),

      由于本例是横向排列的ViewGroup,所以,ViewGroup的高度就是子View里最高的那个子View的高度再加上上下padding了

      代码里的:

      desireWidth=所有子View的宽度累加之和+左右padding

      desireHeight=最高的那个子View的高度+上下padding

  3. onMeasure模板代码:(只是支持wrap_content和padding,还不支持margin)

    ` @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
        // ★1. 计算所有child view 要占用的空间
        desireWidth = 0;//累加所有子View的宽度,作为MyViewGroup宽度设为wrap_content时的宽度
        desireHeight = 0;//本例子是横向排列的ViewGroup,所以,MyViewGroup高度设为wrap_content时,其高度是其所有子View中最高子View的高度
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View v = getChildAt(i);
    
            //1.1 开始遍历测量所有的子View,并且根据实际情况累加子View的宽(或者高),为了计算整个viewgroup的宽度(高度)
            if (v.getVisibility() != View.GONE) {//不去测量Gone的子View
                measureChild(v, widthMeasureSpec,
                        heightMeasureSpec);
    
                --------------只需要根据你的需求,更改虚线内部的代码即可,其余的不用改--------------
                //由于横向排列,累加所有的子View的宽度
                desireWidth += v.getMeasuredWidth();
                //高度是子View中最高的高度
                desireHeight = Math
                        .max(desireHeight, v.getMeasuredHeight());
                --------------只需要根据你的需求,更改虚线内部的代码即可,其余的不用改--------------
    
            }
        }
    
        // 1.2 考虑padding值
        //到目前为止desireWidth为所有子View的宽度的累加,作为MyViewGroup的总宽度,要加上左右padding值
        desireWidth += getPaddingLeft() + getPaddingRight();
        //高度同理略
        desireHeight += getPaddingTop() + getPaddingBottom();
    
    
        //★2.测量ViewGroup的宽高,如果不写这一步,使用wrap_content时效果为match_parent的效果
        // (下面的写法比较简洁,《android群英传》介绍了另外一种写法,比这个稍微麻烦一点)
        // see if the size is big enough
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
        // super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
                resolveSize(desireHeight, heightMeasureSpec));
    }`
    

onLayout要实现的内容

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

            //这个自定义的ViewGroup的有效内容的四个边界
            final int parentLeft = getPaddingLeft();//这个MyViewGroup的最左边的距离(纯粹的内容的左边界,不含padding)
            final int parentTop = getPaddingTop();//这个MyViewGroup的最上边的距离(纯粹的内容的上边界,不含padding)
            final int parentRight = r - l - getPaddingRight();//这个MyViewGroup的最右边的距离(纯粹的内容的右边界,不含padding)

            final int parentBottom = b - t - getPaddingBottom();//这个MyViewGroup的最下边的距离(纯粹的内容的下边界,不含padding)

            if (BuildConfig.DEBUG)
                Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
                        + parentTop + "   parentright: " + parentRight
                        + "   parentbottom: " + parentBottom+"\n"+ "   l: " + l+ "   t: " + t+ "   r: " + r+ "   b: " + b);

            int left = parentLeft;
            int top = parentTop;

            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                View v = getChildAt(i);
                if (v.getVisibility() != View.GONE) {
                    //得到每一个子View的测量后的宽高
                    final int childWidth = v.getMeasuredWidth();
                    final int childHeight = v.getMeasuredHeight();
                    //开始布局每一个子View(左、上、右=左+子View宽、下=上+子View高)
                    v.layout(left, top, left + childWidth, top + childHeight);
                    //由于本例是横向排列的,所以每一个子View的left值要递增
                    left += childWidth;
                }
            }
}`

完整代码:

`
    public class MyViewGroup extends ViewGroup {

    private int desireWidth;
    private int desireHeight;

    public MyViewGroup(Context context) {
        this(context, null);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        // ★1. 计算所有child view 要占用的空间
        desireWidth = 0;//累加所有子View的宽度,作为MyViewGroup宽度设为wrap_content时的宽度
        desireHeight = 0;//本例子是横向排列的ViewGroup,所以,MyViewGroup高度设为wrap_content时,其高度是其所有子View中最高子View的高度
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View v = getChildAt(i);

            //1.1 开始遍历测量所有的子View,并且根据实际情况累加子View的宽(或者高),为了计算整个viewgroup的宽度(高度)
            if (v.getVisibility() != View.GONE) {//不去测量Gone的子View
                measureChild(v, widthMeasureSpec,
                        heightMeasureSpec);
                //由于横向排列,累加所有的子View的宽度
                desireWidth += v.getMeasuredWidth();
                //高度是子View中最高的高度
                desireHeight = Math
                        .max(desireHeight, v.getMeasuredHeight());
            }
        }

        // 1.2 考虑padding值
        //到目前为止desireWidth为所有子View的宽度的累加,作为MyViewGroup的总宽度,要加上左右padding值
        desireWidth += getPaddingLeft() + getPaddingRight();
        //高度同理略
        desireHeight += getPaddingTop() + getPaddingBottom();


        //★2.测量ViewGroup的宽高,如果不写这一步,使用wrap_content时效果为match_parent的效果
        // (下面的写法比较简洁,《Android群英传》介绍了另外一种写法,比这个稍微麻烦一点)
        // see if the size is big enough
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
        //super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
                resolveSize(desireHeight, heightMeasureSpec));
    }

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


        //这个自定义的ViewGroup的有效内容的四个边界
        final int parentLeft = getPaddingLeft();//这个MyViewGroup的最左边的距离(纯粹的内容的左边界,不含padding)
        final int parentTop = getPaddingTop();//这个MyViewGroup的最上边的距离(纯粹的内容的上边界,不含padding)
        final int parentRight = r - l - getPaddingRight();//这个MyViewGroup的最右边的距离(纯粹的内容的右边界,不含padding)
        final int parentBottom = b - t - getPaddingBottom();//这个MyViewGroup的最下边的距离(纯粹的内容的下边界,不含padding)

        if (BuildConfig.DEBUG)
            Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
                    + parentTop + "   parentright: " + parentRight
                    + "   parentbottom: " + parentBottom + "\n" + "   l: " + l + "   t: " + t + "   r: " + r + "   b: " + b);

        int left = parentLeft;
        int top = parentTop;

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View v = getChildAt(i);
            if (v.getVisibility() != View.GONE) {
                //得到每一个子View的测量后的宽高
                final int childWidth = v.getMeasuredWidth();
                final int childHeight = v.getMeasuredHeight();
                //开始布局每一个子View(左、上、右=左+子View宽、下=上+子View高)
                v.layout(left, top, left + childWidth, top + childHeight);
                //由于本例是横向排列的,所以每一个子View的left值要递增
                left += childWidth;
            }
        }
    }
}

`

总结:

一般情况,自定义ViewGroup支持wrap-content和padding就够用了

第二步、自己来写一个横向的LinearLayout(实现支持子View的margin)

每一个自定义ViewGroup都必须,自定义这个类LayoutParams,以及后面的三个方法,否则强转报异常,
其余没什么好说的,计算距离,布局的时候把margin考虑进去即可

`public class MyViewGroupMargin extends ViewGroup {

    private int desireWidth;
    private int desireHeight;

    public MyViewGroupMargin(Context context) {
        this(context, null);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        // ★1. 计算所有child view 要占用的空间
        desireWidth = 0;//累加所有子View的宽度,作为MyViewGroup宽度设为wrap_content时的宽度
        desireHeight = 0;//本例子是横向排列的ViewGroup,所以,MyViewGroup高度设为wrap_content时,其高度是其所有子View中最高子View的高度
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View v = getChildAt(i);
            //1.1 开始遍历测量所有的子View,并且根据实际情况累加子View的宽(或者高),为了计算整个viewgroup的宽度(高度)
            if (v.getVisibility() != View.GONE) {//不去测量Gone的子View

                LayoutParams lp = (LayoutParams) v.getLayoutParams();

                //▲变化1:将measureChild改为measureChildWithMargin
                measureChildWithMargins(v, widthMeasureSpec, 0,
                        heightMeasureSpec, 0);
              /*原来:  measureChild(v, widthMeasureSpec,
                        heightMeasureSpec);*/

                //▲变化2:这里在累加所有的子View的宽度时加上他自己的margin
                desireWidth += v.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
                desireHeight = Math
                        .max(desireHeight, v.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);

                /*原来://由于横向排列,累加所有的子View的宽度
                desireWidth += v.getMeasuredWidth();
                //高度是子View中最高的高度
                desireHeight = Math
                        .max(desireHeight, v.getMeasuredHeight());*/
            }
        }

        // 1.2 考虑padding值
        //到目前为止desireWidth为所有子View的宽度的累加,作为MyViewGroup的总宽度,要加上左右padding值
        desireWidth += getPaddingLeft() + getPaddingRight();
        //高度同理略
        desireHeight += getPaddingTop() + getPaddingBottom();


        //★2.测量ViewGroup的宽高,如果不写这一步,使用wrap_content时效果为match_parent的效果
        // (下面的写法比较简洁,《Android群英传》介绍了另外一种写法,比这个稍微麻烦一点)
        // see if the size is big enough
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
                resolveSize(desireHeight, heightMeasureSpec));
    }



    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int parentLeft = getPaddingLeft();
        final int parentRight = r - l - getPaddingRight();
        final int parentTop = getPaddingTop();
        final int parentBottom = b - t - getPaddingBottom();

        if (BuildConfig.DEBUG)
            Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
                    + parentTop + "   parentright: " + parentRight
                    + "   parentbottom: " + parentBottom);

        int left = parentLeft;
        int top = parentTop;

        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View v = getChildAt(i);
            if (v.getVisibility() != View.GONE) {
                LayoutParams lp = (LayoutParams) v.getLayoutParams();
                final int childWidth = v.getMeasuredWidth();
                final int childHeight = v.getMeasuredHeight();


                //▲变化1:左侧要加上这个子View的左侧margin
                left += lp.leftMargin;
                //▲变化2:上侧要加上子View的margin
                top = parentTop + lp.topMargin;

                if (BuildConfig.DEBUG) {
                    Log.d("onlayout", "child[width: " + childWidth
                            + ", height: " + childHeight + "]");
                    Log.d("onlayout", "child[left: " + left + ", top: "
                            + top + ", right: " + (left + childWidth)
                            + ", bottom: " + (top + childHeight));
                }
                v.layout(left, top, left + childWidth, top + childHeight);
                //▲变化3:因为是横向排列的,所以下一个View的左侧加上这个view的右侧的margin(如果是纵向排列的则对应改变top)
                left += childWidth + lp.rightMargin;

            }
        }
    }

    //★★★★★★★★★★★★★★★★★★★★★★★要使用margin必须写下面的方法★★★★★★★★★★★★★★★★★★★★★
    //***开始***每一个自定义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 LayoutParams(MarginLayoutParams source) {
            super(source);
        }
    }

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

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

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(
            ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }
    //***结束***每一个自定义ViewGroup都必须,自定义这个类LayoutParams,以及后面的三个方法,否则强转报异常,模板代码照抄即可**************
}

`

以上是关于自定义ViewGroup的主要内容,如果未能解决你的问题,请参考以下文章

自定义ViewGroup获取子View参数

Android 自定义ViewGroup之实现FlowLayout-标签流容器

Android自定义 View 系列-ViewGroup

Android自定义 View 系列实战篇-ViewGroup

Android自定义 View 系列实战篇-ViewGroup

自定义控件自定义ViewGroup 在ViewGroup中显示TextView