ViewGroup的简略流式布局

前言

前面几篇咱们简略的温习了一下自界说 View 的丈量与制作,而且回忆了常见的一些事情的处理办法。

那么假如咱们想自界说 ViewGroup 的话,它和自界说View又有什么区别呢?其实咱们把 ViewGroup 当做 View 来用的话也不是不能够。可是已然咱们用到了容器 ViewGroup 其时是想用它的一些特殊的特性了。

比方 ViewGroup 的丈量,ViewGroup的布局,ViewGroup的制作。

  1. ViewGroup的丈量:与 View 的丈量不同,ViewGroup 的丈量会遍历子 View ,获取子 View 的巨细,从而决议自己的巨细。当然咱们也能够经过指定的形式来指定自身的巨细。
  2. ViewGroup的布局:这个是 ViewGroup 中心与常用的功能。找到对于的子View 布局到指定的方位。
  3. ViewGroup的制作:一般咱们不会重写这个办法,由于一般来说它本身不需求制作,而且当咱们没有设置ViewGroup的背景的时分,onDraw()办法都不会被调用,一般来说 ViewGroup 仅仅会运用 dispatchDraw()办法来制作其子View,其过程同样是经过遍历一切子View,并调用子View的制作办法来完结制作作业。

下面咱们一同温习一下ViewGroup的丈量布局办法。咱们以入门级的 FlowLayout 为例,看看流式布局是如何丈量与布局的。

话不多说,Let’s go

Android自定义ViewGroup的布局,往往都是从流式布局开始

一、基本的丈量与布局

咱们先回忆一下ViewGroup的

一个经典的ViewGroup丈量是怎样完结?一般来说,最简略的丈量如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for(int i = 0; i < getChildCount(); i++){
          View childView = getChildAt(i);
          measureChild(childView,widthMeasureSpec,heightMeasureSpec);
        }
    }

或许咱们直接运用封装之后的默许办法

measureChildren(widthMeasureSpec,heightMeasureSpec);

其内部也是遍历子View来完结的。当然假如有自界说的一些宽高丈量规矩,就不能运用这个办法,就需求自己遍历找到View自界说完结了。

需求留意的是,这儿咱们丈量子布局传递的 widthMeasureSpec 和 heightMeasureSpec 是父布局的丈量形式。

当父布局设置为固定宽度的时分,子View是不能超过这个宽度的,比方父控件设置为match_parent,自界说View无论是match_parent 仍是 wrap_content 都是相同的,充满整个父控件。

相当于父布局调用子控件的onMeasure办法的时分告知子控件,我就这么大,你看着办,不能超过它。

而父布局传递的是自适应AT_MOST形式,那么便是由子View来决议父布局的宽高。

相当于父布局调用子控件的onMeasure办法的时分问子控件,我也不知道我多大,你需求多大的方位?我又需求多大的当地才能包容你?

其实也很好了解。那么一个经典的ViewGroup布局又是怎样完结?重写 onLayout 而且遍历拿到每一个View,进行Layout操作。

比方如下的代码,咱们每一个View的高度设置为固定高度,而且笔直排列,相似一个ListView 的布局:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        //设置子View的高度
        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
        params.height = mFixedHeight * childCount;
        setLayoutParams(params);
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                child.layout(l, i * mFixedHeight, r, (i + 1) * mFixedHeight);
            }
        }
    }

留意咱们 onLayout() 的参数

展现的作用便是这样:

Android自定义ViewGroup的布局,往往都是从流式布局开始

二、流式的布局的layout

首要咱们先不管丈量,咱们先指定ViewGroup的宽高为固定宽高,指定为match_parent。咱们先做布局的操作:

Android自定义ViewGroup的布局,往往都是从流式布局开始

咱们自界说 ViewGroup 中重写丈量与布局的办法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    /**
     * @param changed 当时ViewGroup的尺度或许方位是否发生了改动
     * @param l,t,r,b 当时ViewGroup相对于父控件的坐标方位,
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int mViewGroupWidth = getMeasuredWidth(); //当时ViewGroup的总宽度
        int layoutChildViewCurX = l; //当时制作View的X坐标
        int layoutChildViewCurY = t; //当时制作View的Y坐标
        int childCount = getChildCount(); //子控件的数量
        //遍历一切子控件,并在其方位上制作子控件
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //子控件的宽和高
            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();
            //假如剩下控件不行,则移到下一行开端方位
            if (layoutChildViewCurX + width > mViewGroupWidth) {
                layoutChildViewCurX = l;
                //假如换行,则需求修正当时制作的高度方位
                layoutChildViewCurY += height;
            }
            //执行childView的布局与制作(右和下的方位加上自身的宽高即可)
            childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + height);
            //布局完结之后,下一次制作的X坐标需求加上宽度
            layoutChildViewCurX += width;
        }
    }

终究咱们就能得到对应的换行作用,如下:

Android自定义ViewGroup的布局,往往都是从流式布局开始

经过上面咱们的基础学习,咱们应该能了解这样的布局办法,跟上面的基础布局办法比较,便是多了一个 layoutChildViewCurX 和 layoutChildViewCurY 。关于其它的逻辑这儿已经注释的非常清楚了。

可是这样的作用好丑,咱们加上距离 margin 试试?

Android自定义ViewGroup的布局,往往都是从流式布局开始

并没有作用,其实是内部 View 的 LayoutParams 就不支撑 margin,咱们需求界说一个内部类承继 ViewGroup.MarginLayoutParams,并重写generateLayoutParams() 办法。

    //要使子控件的margin特点有效必须承继此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 layoutParams) {
            super(layoutParams);
        }
    }
    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new ViewGroup2.LayoutParams(getContext(), attrs);
    }
    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

然后修正一下代码,在 layout 子布局的时分咱们手动的把 margin 加上。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int mViewGroupWidth = getMeasuredWidth(); //当时ViewGroup的总宽度
        int layoutChildViewCurX = l; //当时制作View的X坐标
        int layoutChildViewCurY = t; //当时制作View的Y坐标
        int childCount = getChildCount(); //子控件的数量
        //遍历一切子控件,并在其方位上制作子控件
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //子控件的宽和高
            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();
            final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
            //假如剩下控件不行,则移到下一行开端方位
            if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > mViewGroupWidth) {
                layoutChildViewCurX = l;
                //假如换行,则需求修正当时制作的高度方位
                layoutChildViewCurY += height + lp.topMargin + lp.bottomMargin;
            }
            //执行childView的布局与制作(右和下的方位加上自身的宽高即可)
            childView.layout(
                    layoutChildViewCurX + lp.leftMargin,
                    layoutChildViewCurY + lp.topMargin,
                    layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin,
                    layoutChildViewCurY + height + lp.topMargin + lp.bottomMargin);
            //布局完结之后,下一次制作的X坐标需求加上宽度
            layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
        }
    }

此刻的作用就能收效了:

Android自定义ViewGroup的布局,往往都是从流式布局开始

三、流式的布局的Measure

前面的设置咱们都是运用的宽高 match_parent。那咱们修正 ViewGroup 的高度为 wrap_content ,能完结高度自适应吗?

Android自定义ViewGroup的布局,往往都是从流式布局开始

这…并不是咱们想要的作用。并没有自适应高度。由于咱们没有写丈量的逻辑。

咱们想一下,假如咱们的宽度是固定的,想要高度自适应,那么咱们就需求丈量每一个子View的高度,计算出对应的高度,当换行之后咱们再加上行的高度。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft();
        final int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        final int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - this.getPaddingTop() - this.getPaddingBottom();
        final int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.EXACTLY) {
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.AT_MOST) {
            int layoutChildViewCurX = this.getPaddingLeft();
            int totalControlHeight = 0;
            for (int i = 0; i < getChildCount(); i++) {
                final View childView = this.getChildAt(i);
                if (childView.getVisibility() == GONE) {
                    continue;
                }
                final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
                childView.measure(
                        getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
                        getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
                );
                int width = childView.getMeasuredWidth();
                int height = childView.getMeasuredHeight();
                if (totalControlHeight == 0) {
                    totalControlHeight = height + lp.topMargin + lp.bottomMargin;
                }
                //假如剩下控件不行,则移到下一行开端方位
                if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
                    layoutChildViewCurX = this.getPaddingLeft();
                    totalControlHeight += height + lp.topMargin + lp.bottomMargin;
                }
                layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
            }
            //终究确认整个布局的高度和宽度
            int cachedTotalWith = resolveSize(sizeWidth, widthMeasureSpec);
            int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);
            this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);
        }

宽度固定和高度自适应的情况下,咱们是这么处理的。计算出子View的总高度,然后设置 setMeasuredDimension 为ViewGroup的丈量宽度和子View的总高度。即为终究 ViewGroup 的宽高。

Android自定义ViewGroup的布局,往往都是从流式布局开始

这样咱们就能完结高度的自适应了。那么宽度能不能自适应呢?

当然能够,咱们只需求记载每一行的宽度,然后终究 setMeasuredDimension 的时分传入一切行中的最大宽度,便是 ViewGroup 的终究宽度,而高度的计算是和上面的办法相同的。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
       else if (modeWidth == MeasureSpec.AT_MOST && modeHeight == MeasureSpec.AT_MOST) {
            //假如宽高都是Wrap-Content
            int layoutChildViewCurX = this.getPaddingLeft();
            //总宽度和总高度
            int totalControlWidth = 0;
            int totalControlHeight = 0;
            //由于宽度对错固定的,所以用一个List接收每一行的最大宽度
            List<Integer> lineLenghts = new ArrayList<>();
            for (int i = 0; i < getChildCount(); i++) {
                final View childView = this.getChildAt(i);
                if (childView.getVisibility() == GONE) {
                    continue;
                }
                final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
                childView.measure(
                        getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
                        getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
                );
                int width = childView.getMeasuredWidth();
                int height = childView.getMeasuredHeight();
                if (totalControlHeight == 0) {
                    totalControlHeight = height + lp.topMargin + lp.bottomMargin;
                }
                //假如剩下控件不行,则移到下一行开端方位
                if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
                    lineLenghts.add(layoutChildViewCurX);
                    layoutChildViewCurX = this.getPaddingLeft();
                    totalControlHeight += height + lp.topMargin + lp.bottomMargin;
                }
                layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
            }
            //计算每一行的宽度,选出最大值
            YYLogUtils.w("每一行的宽度 :" + lineLenghts.toString());
            totalControlWidth = Collections.max(lineLenghts);
            YYLogUtils.w("选出最大宽度 :" + totalControlWidth);
            //终究确认整个布局的高度和宽度
            int cachedTotalWith = resolveSize(totalControlWidth, widthMeasureSpec);
            int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);
            this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);
        }
    }

为了作用,咱们把榜首行的终究一个View宽度多一点,便利查看作用。

Android自定义ViewGroup的布局,往往都是从流式布局开始

这样就能够得到ViewGroup自适应的宽度和高度了。并不杂乱对不对!

后记

这样是不是就能完结一个简略的流式布局了呢?当然这些仅仅为便利学习和了解,真实的实战中并不推荐直接这样运用,由于内部还有一些兼容的逻辑没处理,一些逻辑没有封装,特点没有抽取。甚至连每一个View的高度,和每一行的最大高度也没有处理,其实这样健壮性并不好。

假如我们想要在项目中运用流式布局,那么我仍是推荐运用鸿洋的流式布局【传送门】。

或许运用谷歌官方的流式布局 FlexboxLayout 【传送门】

丈量与布局是 ViewGroup 的基本功了,把握了流式布局之后,咱们对其他的一些 Viewgroup 布局就能快速下手了。

关于本文的内容假如想查看源码能够点击这儿 【传送门】。你也能够重视我的这个Kotlin项目,我有时间都会持续更新。

惯例,我如有解说不到位或错漏的当地,希望同学们能够指出沟通。

假如感觉本文对你有一点点的启发,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

Android自定义ViewGroup的布局,往往都是从流式布局开始

本文正在参与「金石方案 . 分割6万现金大奖」