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

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

一、AutoFlowLayout应用场景

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

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

二、AutoFlowLayout实现效果

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

1.功能

流式布局

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

网格布局

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

    2.效果

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


    网格布局

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

Github地址:AutoFlowLayout

三、AutoFlowLayout使用

1.添加依赖

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

1
2
3
4
5
6
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

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

1
2
3
dependencies {
compile 'com.github.LRH1993:AutoFlowLayout:1.0.2'
}

2.属性说明

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

3.使用示例

布局

1
2
3
4
5
6
7
8
9
<?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>

代码设置数据

1
2
3
4
5
6
7
8
9
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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的宽高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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;
//统计最大高度/最大宽度
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();
maxWidth +=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);
}
}
}
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布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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吧!