AutoFlowLayout:多功能流式布局与网格布局控件

来源:互联网 发布:阿里云企业域名备案 编辑:程序博客网 时间:2024/04/28 02:34

近期工作需要用到流式布局,网上也有很多关于这方面的资料。发现流式布局与网格布局的自定义很有意思,是学习自定义控件的一个很好的方式,所以就撸了个几百行代码的控件,既实用又具有学习价值。

一、AutoFlowLayout应用场景

流式布局,在很多标签类的场景中可以用的;而网格布局在分类中以及自拍九宫格等场景很常见。如下所示:

如此使用频繁而又实现简单的控件,怎能不自己撸一个呢?控件,还是定制的好啊。

二、AutoFlowLayout实现效果

先介绍下自己撸的这个控件的功能及效果。

1.功能

流式布局
- 自动换行
- 行数自定:单行/多行
- 支持单选/多选
- 支持行居中/靠左显示
- 支持添加/删除子View
- 支持子View点击/长按事件

网格布局
- 行数/列数自定
- 支持单选/多选
- 支持添加/删除子View
- 支持子View点击/长按事件
- 支持添加多样式分割线及横竖间隔

2.效果

下面以gif图的形式展现下实现的效果,样式简单了些,不过依然能展示出这个简单控件的多功能实用性。
流式布局


网格布局

最后一个是带间隔以及分割线的,由于录屏原因,只在跳过去的一瞬间显示了粉红色的一条线。真实如下图所示,可以定义横竖间距的大小,以及分割线的颜色,宽度。

Github地址:AutoFlowLayout

三、AutoFlowLayout使用

1.添加依赖

①.在项目的 build.gradle 文件中添加

allprojects {        repositories {            ...            maven { url 'https://jitpack.io' }        }    }

②.在 module 的 build.gradle 文件中添加依赖

dependencies {            compile 'com.github.LRH1993:AutoFlowLayout:1.0.5'    }

2.属性说明

下表是自定义的属性说明,可在xml中声明,同时有对应的get/set方法,可在代码中动态添加。

3.使用示例

布局

<?xml version="1.0" encoding="utf-8"?>    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"                    android:layout_width="match_parent"                    android:layout_height="match_parent">    <com.example.library.AutoFlowLayout        android:id="@+id/afl_cotent"        android:layout_width="match_parent"        android:layout_height="wrap_content"/></RelativeLayout>

代码设置数据

mFlowLayout.setAdapter(new FlowAdapter(Arrays.asList(mData)) {            @Override            public View getView(int position) {                View item = mLayoutInflater.inflate(R.layout.special_item, null);                TextView tvAttrTag = (TextView) item.findViewById(R.id.tv_attr_tag);                tvAttrTag.setText(mData[position]);                return item;            }        });

与ListView,GridView使用方式一样,实现FlowAdapter即可。

四、AutoFlowLayout原理

ViewGroup的测量、布局及绘制顺序如下所示:

详细的自定义View原理参考:图解View测量、布局及绘制原理

下面具体介绍自定义实现网格布局的过程。

1.重写generateLayoutParams()方法

因为我们要在onMeasure以及onLayout的过程中,测量子View的margin,所以要重写该方法,并返回MarginLayoutParams。

@Override    public LayoutParams generateLayoutParams(AttributeSet attrs)    {        return new MarginLayoutParams(getContext(), attrs);    }    @Override    protected LayoutParams generateLayoutParams(LayoutParams p) {        return new MarginLayoutParams(p);    }    @Override    protected LayoutParams generateDefaultLayoutParams() {        return new MarginLayoutParams(super.generateDefaultLayoutParams());    }

2.onMeasure过程

主要针对wrap_content情况下,要逐行逐列的测量每个子View的宽高,padding,margin以及横竖间距,来获得最终ViewGroup的宽高。

private void setGridMeasure(int widthMeasureSpec, int heightMeasureSpec) {        // 获得它的父容器为它设置的测量模式和大小        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);        //获取viewgroup的padding        int paddingLeft = getPaddingLeft();        int paddingRight = getPaddingRight();        int paddingTop = getPaddingTop();        int paddingBottom = getPaddingBottom();        //最终的宽高值        int heightResult;        int widthResult;        //未设置行数 推测行数        if (mRowNumbers == 0) {            mRowNumbers = getChildCount()%mColumnNumbers == 0 ?                    getChildCount()/mColumnNumbers : (getChildCount()/mColumnNumbers + 1);        }        int maxChildHeight = 0;        int maxWidth = 0;        int maxHeight = 0;        int maxLineWidth = 0;        //统计最大高度/最大宽度        for (int i = 0; i <  mRowNumbers; i++) {            for (int j = 0; j < mColumnNumbers; j++) {                final View child = getChildAt(i * mColumnNumbers + j);                if (child != null) {                    if (child.getVisibility() != GONE) {                        measureChild(child,widthMeasureSpec,heightMeasureSpec);                        // 得到child的lp                        MarginLayoutParams lp = (MarginLayoutParams) child                                .getLayoutParams();                        maxLineWidth +=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;                        maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);                    }                }            }            maxWidth = Math.max(maxLineWidth,maxWidth);            maxLineWidth = 0;            maxHeight += maxChildHeight;            maxChildHeight = 0;        }        int tempWidth = (int) (maxWidth+mHorizontalSpace*(mColumnNumbers-1)+paddingLeft+paddingRight);        int tempHeight = (int) (maxHeight+mVerticalSpace*(mRowNumbers-1)+paddingBottom+paddingTop);        if (tempWidth > sizeWidth) {            widthResult = sizeWidth;        } else {            widthResult = tempWidth;        }        //宽高超过屏幕大小,则进行压缩存放        if (tempHeight > sizeHeight) {            heightResult = sizeHeight;        } else {            heightResult = tempHeight;        }        setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth                : widthResult, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight                : heightResult);    }

3.onLayout过程

网格布局默认所有子View的宽高一致,先推算出每个子View的平均宽高,然后逐个推算每个子View的left,top,right,bottom位置,调用child.layout()进行子View布局。

private void setGridLayout() {        mCheckedViews.clear();        mCurrentItemIndex = -1;        int sizeWidth = getWidth();        int sizeHeight = getHeight();        //子View的平均宽高 默认所有View宽高一致        View  tempChild = getChildAt(0);        MarginLayoutParams  lp = (MarginLayoutParams) tempChild                .getLayoutParams();        int childAvWidth = (int) ((sizeWidth - getPaddingLeft() - getPaddingRight() - mHorizontalSpace * (mColumnNumbers-1))/mColumnNumbers)-lp.leftMargin-lp.rightMargin;        int childAvHeight = (int) ((sizeHeight - getPaddingTop() - getPaddingBottom() - mVerticalSpace * (mRowNumbers-1))/mRowNumbers)-lp.topMargin-lp.bottomMargin;        for (int i = 0; i < mRowNumbers; i++) {            for (int j = 0; j < mColumnNumbers; j++) {                final View child = getChildAt(i * mColumnNumbers + j);                if (child != null) {                    mCurrentItemIndex++;                    if (child.getVisibility() != View.GONE) {                        setChildClickOperation(child, -1);                        int childLeft = (int) (getPaddingLeft() + j * (childAvWidth + mHorizontalSpace))+j * (lp.leftMargin + lp.rightMargin) + lp.leftMargin;                        int childTop = (int) (getPaddingTop() + i * (childAvHeight + mVerticalSpace)) + i * (lp.topMargin + lp.bottomMargin) + lp.topMargin;                        child.layout(childLeft, childTop, childLeft + childAvWidth, childAvHeight +childTop);                    }                }            }        }    }

4.dispatchDraw过程

绘制分割线得问过程,需要逐个对子View进行绘制分割线。所以重写dispatchDraw()方法。因为不需要对自己进行绘制,所以不需要重写onDraw()方法。
需要额外注意下,绘制过程中,考虑横竖间距的大小,这种情况下默认不考虑margin。

protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        if (mIsGridMode && mIsCutLine) {            Paint linePaint = new Paint();            linePaint.setStyle(Paint.Style.STROKE);            linePaint.setStrokeWidth(mCutLineWidth);            linePaint.setColor(mCutLineColor);            for (int i = 0; i < mRowNumbers; i++) {                for (int j = 0; j < mColumnNumbers; j++) {                    View child = getChildAt(i * mColumnNumbers + j);                    //最后一列                    if (j == mColumnNumbers-1) {                        //不是最后一行  只画底部                        if (i != mRowNumbers-1){                            canvas.drawLine(child.getLeft()-mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,                                    child.getRight(),child.getBottom()+mVerticalSpace/2,linePaint);                        }                    } else {                        //最后一行 只画右部                        if (i ==  mRowNumbers -1) {                            canvas.drawLine(child.getRight()+mHorizontalSpace/2, child.getTop()-mVerticalSpace/2,                                    child.getRight()+mHorizontalSpace/2,child.getBottom(),linePaint);                        } else {                            //底部 右部 都画                            if (j == 0) {                                canvas.drawLine(child.getLeft(),child.getBottom()+mVerticalSpace/2,                                        child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);                            } else {                                canvas.drawLine(child.getLeft()-mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,                                        child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);                            }                            if (i == 0) {                                canvas.drawLine(child.getRight()+mHorizontalSpace/2, child.getTop(),                                        child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);                            } else {                                canvas.drawLine(child.getRight()+mHorizontalSpace/2, child.getTop()-mVerticalSpace/2,                                        child.getRight()+mHorizontalSpace/2,child.getBottom()+mVerticalSpace/2,linePaint);                            }                        }                    }                }            }        }    }

绘制流式标签的过程类似,一样的简单。不过通过实现的过程,确实加深了对自定义ViewGroup的理解。

Github地址:https://github.com/LRH1993/AutoFlowLayout
点个star,一起来学习自定义ViewGroup吧!

原创粉丝点击