Android 自定义简单ViewGroup

一、前言

前面我们讲了怎么自定义简单View,其实原理都很简单,思路就那几步,难的是怎么定义大型的复杂的View,因为那往往意味着大量的内部逻辑和有着非常高的数学要求,但也不用怕,自定义之路其实就是这样,平时多多练习,多思考,多接触一些别人写的开源控件,能力就会慢慢的自然而然的提升上来的,而我们今天说的ViewGroup也同样如此。

二、相关介绍

ViewGroup就是一个盛放childView(可能是一个ViewGroup,也可能是View)的容器,我们在xml中经常使用的RelativeLayout、LinearLayout、FrameLayout、AbsoluteLayout都是ViewGroup的子类,它负责给childView计算出建议的宽高尺寸和测量模式,并且确定childView在其所提供区域内的位置,Android API中是这样介绍ViewGroup的

这里写图片描述

前两句跟我们上面说的是一个意思,但是API在后面还着重提到了ViewGroup.LayoutParams ,其又是何许东西也?

这里写图片描述

看了API介绍之后是不是有点理解了,LayoutParams被childView用来告诉它们的容器它们该如何布局,因为每个childView不同,所以需求就有可能不同,其编写的属性就会存在差异,就如我们常用的RelativeLayout、LinearLayout中的layout_centerInParent和layout_weight属性各自独有一样。所以这也就是为什么ViewGroup让其childView自己实现自己所需要的LayoutParams的原因,ViewGroup.LayoutParams只给我们提供了最基本的layout_width和layout_height,因为这是每个ViewGroup所通用的layout_属性。

三、自定义ViewGroup

首先我们先总结一下一般自定义ViewGroup的步骤,让我们在写的时候能有一个大致的方向:

1.继承一个ViewGroup或者一个我们需要的ViewGroup的子类,并添加构造方法
2.重写onMeasure方法
3.重写onLayout方法
4.在xml中自定义ViewGroup属性

关于定义ViewGroup属性的顺序,我原来已经说过查看,看个人习惯吧。闲话不多说了,我们直接来拿一个流式布局的小例子来从头到尾的实现一下吧,效果图如下:

这里写图片描述

这是我们自定义的ViewGroup类,我们添加了不同参数的构造方法

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

    public MyFlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

我们要重写onMeasure方法,ViewGroup不仅要给childView传入测量值和测量模式,而且还要求我们自己去测量尺寸并提供给父ViewGroup让其给我们提供期望大小的区域,其中测量模式分为3种:

1.EXACTLY:提供的尺寸就是当前view所应该取的尺寸,一般设置了明确的固定大小或者match_parent
2.AT_MOST:提供的尺寸就是当前view所能取的最大尺寸,一般设置了wrap_content
3.UNSPECIFIED:父view对当前view的尺寸没有限制,当前view想要多大都可以,一般很少用到

来看看我们的onMeasure方法,对于layout_width和layout_height的不同情况做了相应的处理

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //计算出所有child的宽度和高度
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //计算child的个数
        int childCount = getChildCount();
        //viewGroup最终要提供的宽度和高度
        int maxWidth = 0;
        int maxHeight = 0;

        int widthModle = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightModle = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        switch (widthModle) {
            //当宽度为wrap_content
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                int childWidth = 0;
                for(int i = 0; i< childCount; i++){
                    //将每个child的宽度累加
                    childWidth += getChildAt(i).getMeasuredWidth();
                    //如果一行放不下,则另起一行算起
                    if(childWidth > widthSize){
                        //则viewGrop提供的宽度只需要是每行最大的宽度即可
                        maxWidth = Math.max(childWidth - getChildAt(i).getMeasuredWidth(),maxWidth);
                        //宽度从头开始
                        childWidth = getChildAt(i).getMeasuredWidth();
                    }
                    else{
                        maxWidth = Math.max(childWidth,maxWidth);
                    }
                }
                break;
            case MeasureSpec.EXACTLY:
                //当宽度全屏或者固定尺寸时候
                maxWidth = widthSize;
                break;
        }

        switch (heightModle) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                int childWidth = 0;
                int childHeight = 0;
                int beforeMaxHeight = 0;
                for(int i = 0; i< childCount; i++){
                    //将每个child的宽度累加
                    childWidth += getChildAt(i).getMeasuredWidth();
                    //获取当前child的高度
                    childHeight = getChildAt(i).getMeasuredHeight();
                    //如果一行放不下,则另起一行算起
                    if(childWidth > widthSize){
                        //则viewGrop提供的宽度只需要是每行最大的宽度即可
                        maxWidth = Math.max(childWidth - getChildAt(i).getMeasuredWidth(),maxWidth);
                        //宽度从头开始
                        childWidth = getChildAt(i).getMeasuredWidth();
                        //开始下行之前,记录下当前的高度
                        beforeMaxHeight = maxHeight;
                        //换行的时候,高度也要变成多行的高度
                        maxHeight += childHeight;
                    }
                    else{
                        //每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
                        maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
                    }
                }
                break;
            case MeasureSpec.EXACTLY:
                //当高度全屏或者固定尺寸时候
                maxHeight = heightSize;
                break;
        }

        //给viewGroup设置最终提供的大小
        setMeasuredDimension(maxWidth, maxHeight);
    }

重写onLayout方法,给childView确定位置,其实核心方法就是childView.layout(l, t, r, b),原理非常简单,难的还是那句话,是每个childView区域坐标的求法。

    @Override
    protected void onLayout(boolean f, int l, int t, int r, int b) {
        //计算child的个数
        int childCount = getChildCount();

        int maxHeight = 0;
        int childWidth = 0;
        int childHeight = 0;
        int beforeMaxHeight = 0;

        for(int i = 0; i< childCount; i++){
            View childView = getChildAt(i);
            //将每个child的宽度累加
            childWidth += childView.getMeasuredWidth();
            //获取当前child的高度
            childHeight = childView.getMeasuredHeight();
            //如果一行放不下,则另起一行算起
            if(childWidth > getWidth()){
                //宽度从头开始算
                childWidth = childView.getMeasuredWidth();
                //开始下行之前,记录下当前的高度
                beforeMaxHeight = maxHeight;
                //换行的时候,高度也要变成多行的高度
                maxHeight += childHeight;
                //换行的时候给child确定位置
                childView.layout(0, beforeMaxHeight, childWidth, maxHeight);
            }
            else{
                //每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
                maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
                //给每个child确定位置
                childView.layout(childWidth - childView.getMeasuredWidth(), beforeMaxHeight, childWidth, childHeight+beforeMaxHeight);
            }
        }
    }

好了,到这里你会发现ViewGroup已经完成了,但是这里没有写自定义属性,有兴趣的可以查看上一篇文章了解一下,其实在开发中为了更灵活的使用ViewGroup,childView往往都需要支持一些布局属性,最常见的就是margin,所以下面我们就来看下怎么让childView支持margin属性,其实其他属性也大同小异啦。

四、支持Margin

我们需要支持什么属性,只需要去继承相应的LayoutParams即可,在这里我们要实现margin效果,所以只需要继承MarginLayoutParams,因为在ViewGroup源码中已经给我们提供了静态内部类MarginLayoutParams,所以我们只需要重写

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

重写onMeasure方法,思路跟上面一样,只不过这次在计算时需要加上childView可能存在的margin外边距,所以这就告诉我们,在需求下来之后,一定要好好考虑要不要支持边距,如果对方也不确定,那就加上,要不等弄好之后还得梳理代码,那是真的超烦唉。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //计算child的个数
        int childCount = getChildCount();
        //计算出所有child的宽度和高度
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //viewGroup最终要提供的宽度和高度
        int maxWidth = 0;
        int maxHeight = 0;
        //外边距的margin
        MarginLayoutParams params = null;

        int widthModle = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightModle = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        switch (widthModle) {
            //当宽度为wrap_content
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                int childWidth = 0;
                for(int i = 0; i< childCount; i++){
                    View view = getChildAt(i);
                    params = (MarginLayoutParams) view.getLayoutParams();
                    //计算内边距和外边距
                    int excessWidth = params.leftMargin + params.rightMargin;
                    //将每个child的宽度累加
                    childWidth += view.getMeasuredWidth() + excessWidth;
                    //如果一行放不下,则另起一行算起
                    if(childWidth > widthSize){
                        //则viewGrop提供的宽度只需要是每行最大的宽度即可
                        maxWidth = Math.max(childWidth - view.getMeasuredWidth() - excessWidth, maxWidth);
                        //宽度从头开始
                        childWidth = view.getMeasuredWidth() + excessWidth;
                    }
                    else{
                        maxWidth = Math.max(childWidth,maxWidth);
                    }
                }
                break;
            case MeasureSpec.EXACTLY:
                //当宽度全屏或者固定尺寸时候
                maxWidth = widthSize;
                break;
        }

        switch (heightModle) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                int childWidth = 0;
                int childHeight = 0;
                int beforeMaxHeight = 0;
                for(int i = 0; i< childCount; i++){
                    View view = getChildAt(i);
                    params = (MarginLayoutParams) view.getLayoutParams();
                    //计算内边距和外边距
                    int excessWidth = params.leftMargin + params.rightMargin;
                    int excessHeight = params.topMargin + params.bottomMargin;
                    //将每个child的宽度累加
                    childWidth += view.getMeasuredWidth() + excessWidth;
                    //获取当前child的高度
                    childHeight = view.getMeasuredHeight() + excessHeight;
                    //如果一行放不下,则另起一行算起
                    if(childWidth > widthSize){
                        //则viewGrop提供的宽度只需要是每行最大的宽度即可
                        maxWidth = Math.max(childWidth - view.getMeasuredWidth() - excessWidth, maxWidth);
                        //宽度从头开始
                        childWidth = view.getMeasuredWidth() + excessWidth;
                        //开始下行之前,记录下当前的高度
                        beforeMaxHeight = maxHeight;
                        //换行的时候,高度也要变成多行的高度
                        maxHeight += childHeight;
                    }
                    else{
                        //每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
                        maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
                    }
                }
                break;
            case MeasureSpec.EXACTLY:
                //当高度全屏或者固定尺寸时候
                maxHeight = heightSize;
                break;
        }

        //给viewGroup设置最终提供的大小
        setMeasuredDimension(maxWidth, maxHeight);
    }

重写onLayout方法,如果你懂得了原理,最好按着自己的思路写一下,因为这就像小学时的应用题,不同的解题过程和思路都能得到相同的正确结果,所以没有必要非得跟着别人的思路走,但是如果别人的方法确实简单好用,也要懂的吸收借鉴哦。

    @Override
    protected void onLayout(boolean f, int l, int t, int r, int b) {
        //计算child的个数
        int childCount = getChildCount();
        //外边距的margin
        MarginLayoutParams params = null;

        int maxHeight = 0;
        int childWidth = 0;
        int childHeight = 0;
        int beforeMaxHeight = 0;

        for(int i = 0; i< childCount; i++){
            View childView = getChildAt(i);
            params = (MarginLayoutParams) childView.getLayoutParams();
            //将每个child的宽度累加
            childWidth += childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
            //获取当前child的高度
            childHeight = childView.getMeasuredHeight() + params.topMargin + params.bottomMargin;
            //如果一行放不下,则另起一行算起
            if(childWidth > getWidth()){
                //宽度从头开始算
                childWidth = childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
                //开始下行之前,记录下当前的高度
                beforeMaxHeight = maxHeight;
                //换行的时候,高度也要变成多行的高度
                maxHeight += childHeight;
                //换行的时候给child确定位置
                childView.layout(childWidth - childView.getMeasuredWidth() - params.rightMargin
                        , beforeMaxHeight + params.topMargin
                        , childWidth - params.rightMargin
                        , maxHeight - params.bottomMargin);
            }
            else{
                //每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
                maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
                //给每个child确定位置
                childView.layout(childWidth - childView.getMeasuredWidth() - params.rightMargin
                        , beforeMaxHeight + params.topMargin
                        , childWidth - params.rightMargin
                        , childHeight + beforeMaxHeight - params.bottomMargin);
            }
        }
    }

好了,到这里就已经都介绍完了,Activity代码和xml布局我就不贴了,有兴趣的可以下载demo看下,运行结果如下

这里写图片描述

五、总结

到此我已经基本介绍完自定义简单viewGroup的流程了,但其中涉及到的很多知识点,都没有具体的去深入介绍,充其量只是有个使用方法罢了,如果你感兴趣可以通过切入一个知识点,单独的再去查资料深入剖析一下。

本例源码点击下载

猜你喜欢

转载自blog.csdn.net/MingJieZuo/article/details/80264833