Android 手把手教你View绘制过程--自动排列View的容器

瀑布流

瀑布流大家都太熟悉了,经常听说,最出名的恐怕就是pinterest

这里写图片描述

瀑布流的应用已经很普遍了,最初是开源库的瀑布流,大家用的也是不亦乐乎

开源瀑布流

这里写图片描述

随便一搜,就有很多很成熟的开源的瀑布流

官方瀑布流

可能是鉴于瀑布流的使用确实非常多,在google推出RecyclerView时除了以往的线性布局跟网格布局,也推出了瀑布流布局。

这里写图片描述

API地址

瀑布流的ViewGroup

不管是开源的,还是官方的StaggeredGridLayoutManager,都是列表的瀑布流,也就是说在一整个列表中错落开来达到一个瀑布流的效果。

但是却没有基于ViewGroup的瀑布流。

比如要实现这样的需求

这里写图片描述

这样的设计页面,每一个子View都是不一样的布局,其实就是两层,分组的意思
而开源的瀑布流或者StaggeredGridLayoutManager都是以列表为纬度的,只有一层,就没有办法实现这个需求了。

所以这样的需求,我们只能自己写了。

第一种实现方式 – XML布局中写死

最直接的方式当然就是在XML布局中写死这样的布局,哪一组是9个子View,哪一组是4个子View都是写死的。

这样的写当然很low,而且如果服务器没有返回规定的数据个数,这还会出现空白,总之这样实现太死板,根本无法适应数据的变化。

第二种实现方式–自定义ViewGroup

自定义去排列ViewGroup中的子View,重写onMeasure、onLayout方法,实现自定义测量,自定义布局,自动排列的功能。

先来看看View绘制的知识

这里写图片描述

博客地址

看完上面的博客,我们应该对view绘制有了一定的了解,然后看我们的自定义的ViewGroup


public class ViewContainer extends ViewGroup {
    /*一共几列*/
    public int columns = 2;
    /*是否有分割线,0为没有,1为有分割线,分割线只是子View连接之间的分割线,上下左右边线用padding就好*/
    public int hasDivider = 0;
    /*分割线的宽度*/
    public int dividerWid = 0;
    /*分割线的颜色*/
    public int dividerColor = R.color.background_color;

    public ViewContainer(Context context) {
        super(context);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*屏幕总宽度*/
        int match_parent_wid_value = MeasureSpec.getSize(widthMeasureSpec);
        /*需要的总高度*/
        int total_hei = 0;
        /*当前的宽度*/
        int tempWid = 0;
        /*上一个子View的高度*/
        int beforeHeight = 0;
        /*子View的宽度值*/
        int childWidthValue = 0;
        /*子View的高度值*/
        int childHeiValue = 0;
        /*子View的高度MeasureSpec值*/
        int childHeight = 0;
        /*子View的宽度MeasureSpec值*/
        int childWidth = 0;
        /**计算一共有多少行**/
        int rowCount = 0;
        /*分割线的宽度*/
        int divider= 0;

        if (hasDivider == 0){
            divider = 0;
        }else if (hasDivider == 1){
            divider = dividerWid;
        }
        for (int i = 0; i < getChildCount(); i++) {
            ViewContainerItem view = (ViewContainerItem) getChildAt(i);
            if (view.isMatchParent == 1) {
                childWidthValue = match_parent_wid_value;
            } else {
                /**每个子View的真正的宽度,减去分割线的宽度**/
                childWidthValue = (match_parent_wid_value - (columns - 1) * divider) / columns;
            }
            /*高度是否跟宽度相等,正方形*/
            if (view.isHeiEqualWid == 1) {
                childHeiValue = childWidthValue;
            } else {
                /**如果没有指定宽度和高度相等,那么检查有没有设置radio,宽高比例**/
                if (Math.abs(view.radio - 1.0f) != 0) {
                    childHeiValue = (int) (childWidthValue / view.radio);
                } else {
                    /*什么都没有设置的view,高度等于自身设置的高度*/
                    childHeiValue = view.height;
                }
            }
            /*为每一个子View的宽高计算MeasureSpec值*/
            childWidth = MeasureSpec.makeMeasureSpec(childWidthValue, MeasureSpec.EXACTLY);
            childHeight = MeasureSpec.makeMeasureSpec(childHeiValue, MeasureSpec.EXACTLY);
            /**换行**/
            if (tempWid + childWidthValue > match_parent_wid_value) {
                total_hei += beforeHeight;
                rowCount++;
                tempWid = 0;
            }
            if (view.isMatchParent == 1) {
                view.measure(widthMeasureSpec, childHeight);
                tempWid += match_parent_wid_value;
            } else {
                view.measure(childWidth, childHeight);
                tempWid += childWidthValue;
            }
            beforeHeight = childHeight;
            if (i == getChildCount() - 1) {
                total_hei += beforeHeight;
                rowCount++;
            }
        }
        /**计算完毕后,加上divider的高度**/
        /** 有底部分割线时,分割线的数目等于行数,没有底部分割线时,分割线的数目等于行数减一 **/
        int dividerCount = 0;
        dividerCount = rowCount - 1;
        /*总高度加上分割线的高度*/
        total_hei += dividerCount * divider;
        int totalHei = MeasureSpec.makeMeasureSpec(total_hei, MeasureSpec.EXACTLY);
        /*设置背景色,分割线其实就是背景色透过来的颜色,在onlayout时,每个子View之间有间隙,透过来的缝隙就是分割线*/
        setBackgroundColor(getResources().getColor(dividerColor));
        setMeasuredDimension(widthMeasureSpec, totalHei);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int tempWidth = 0;
        int tempHeight = 0;
        int beforeHeight = 0;
        int divider = 0;
        /*分割线*/
        if (hasDivider == 1) {
            divider = dividerWid;
        } else if (hasDivider == 0){
            divider = 0;
        }
        for (int i = 0; i < getChildCount(); i++) {
            ViewContainerItem view = (ViewContainerItem) getChildAt(i);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            /*如果已经超过了屏幕宽度,则换行*/
            if (tempWidth + width > r) {
                /*换行时,加上上一行的高度,还要加上分割线的高度*/
                tempHeight += beforeHeight+divider;
                /*宽度置为0,从最左边开始布局子view*/
                tempWidth = 0;
            }
            /*占满屏幕宽度的子View*/
            if (view.isMatchParent == 1) {
                view.layout(tempWidth, tempHeight, r, tempHeight + height);
                tempWidth += r;
            } else {
                /*普通子View*/
                view.layout(tempWidth, tempHeight, tempWidth + width, tempHeight + height);
                /*布局完前一个子View,加上前一个View的宽度后,还要加上分割线的宽度
                * 下一次布局时,就留出来了分割线的宽度
                * 因为算的是布局时起始的位置,所以即使是一行中最后一个View也不会因为多加了分割线出问题
                * */
                tempWidth += width + divider;
            }
            /*记录上一个View的高度,如果下一个View在当前行排不开了,需要另起一行,需要加上前一行的高度*/
            beforeHeight = height;
        }
    }
}

代码中的注释都很清楚了,当然就算代码再清楚恐怕我们还是看不太明白到底是怎么能达到自动排列view的目的。

demo

先看下效果图
这里写图片描述

具体代码就不多说了,大家可以下载demo来看看,有了我们自定义的ViewGroup之后,demo的实现就很简单了。

demo Github

希望能帮到你。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值