Android View - 实现流式布局

时间:2021-10-04 05:27:33

流式布局,就是一个容器(ViewGroup),往里面添加元素(子View),元素会一直跟在前一个元素的左边,如果超过容器的边界,就把元素放在下一行的第一个位置。Like This:

Android View - 实现流式布局

我们自己来实现一下这么一种布局,在实现之前,你需要理解关于自定义ViewGroup相关的知识,可以参考 Android 手把手教您自定ViewGroup;如果没问题,接着往下看。

网上已经有很多人实现流式布局了,不过大部分都是静态设置数据,也就是说直接在xml添加子View,像这样:

<com.zhy.zhy_flowlayout02.FlowLayout  
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
style="@style/text_flag_01"
android:text="Welcome" />
<TextView
style="@style/text_flag_01"
android:text="IT工程师" />
<TextView
style="@style/text_flag_01"
android:text="学习ing" />
...
</com.zhy.zhy_flowlayout02.FlowLayout>

出自Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
下面我实现的动态设置子View,像ListView和GridView一样,直接上码!!

流式布局FlowLayout

新建类,继承ViewGroup

// FlowLayout类
public class FlowLayout extends ViewGroup {
/** 子View之间水平距离 */
private int horizontalSpace = 0;
/** 子View之间垂直距离 */
private int verticalSpace = 0;
// 构造
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
horizontalSpace = array.getDimensionPixelOffset(R.styleable.FlowLayout_horizontal_space, 0);
verticalSpace = array.getDimensionPixelOffset(R.styleable.FlowLayout_vertical_space, 0);
array.recycle();
}
}

支持xml定义水平距离和垂直距离。

计算所有子View的高宽(测量)

// FlowLayout类
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 取出宽高的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 取出宽高的大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 子View的个数
int childCount = getChildCount();
// 计算宽度
int calculateWidth = 0;
// 计算高度
int calculateHeight = 0;
// 行宽度
int lineWidth = 0;
// 行高度
int lineHeight = 0;
// 计算,遍历所有子View
for (int index = 0; index < childCount; index++) {
View childView = getChildAt(index);
// 测量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// 子View宽度
int childWidth = childView.getMeasuredWidth() + horizontalSpace;
// 子View高度
int childHeight = childView.getMeasuredHeight();
// 如果 当前行宽 + 当前子View宽度 > 最大宽度
if (childWidth + lineWidth > widthSize) {
// 换行
// 更新计算宽度,如果当前行宽大于之前计算宽度,则计算宽度=当前行宽
calculateWidth = Math.max(lineWidth, calculateWidth);
// 计算高度累加当前行高
calculateHeight += lineHeight + (calculateHeight == 0 ? 0 : verticalSpace);
// 更新当前行宽
lineWidth = childWidth;
// 更新当前行高
lineHeight = childHeight;
} else {
// 不换行
// 累加当前行宽
lineWidth += childWidth;
// 更新当前行高,如果当前子View总高度大于当前行高,则当前行高=当前子View总高度
lineHeight = Math.max(childHeight, lineHeight);
}
// 判断是否最后一个
if (index == childCount - 1) {
// 更新计算宽度,如果当前行宽大于之前计算宽度,则计算宽度=当前行宽
calculateWidth = Math.max(lineWidth, calculateWidth);
// 计算高度累加当前行高
calculateHeight += lineHeight + (calculateHeight == 0 ? 0 : verticalSpace);
}
}
// 如果是EXACTLY模式,直接设置用户指定的宽度
int measureWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : calculateWidth;
// 如果是EXACTLY模式,直接设置用户指定的高度
int measureHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : calculateHeight;
// 设置宽高
setMeasuredDimension(measureWidth, measureHeight);
}

设置所有子View的位置(布局)

// FlowLayout类
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 子View的个数
int childCount = getChildCount();
// 获取最大宽度
int maxWidth = getWidth();
// 行宽
int lineWidth = 0;
// 行高
int lineHeight = 0;
// 记录高度
int recordHeight = 0;
// 布局,遍历所有子View
for (int index = 0; index < childCount; index++) {
View childView = getChildAt(index);
int childWidth = childView.getMeasuredWidth() + horizontalSpace;
int childHeight = childView.getMeasuredHeight();
// 如果 当前行宽 + 当前子View总宽度 > 最大宽度
if (lineWidth + childWidth > maxWidth) {
// 换行
// 记录高度累加当前行高,从第2行起,都要加上垂直距离
recordHeight += lineHeight + (recordHeight == 0 ? 0 : verticalSpace);
// 布局
int childLeft = 0;
int childTop = recordHeight + verticalSpace;
int childRight = childWidth;
int childBottom = recordHeight + childHeight + verticalSpace;
childView.layout(childLeft, childTop, childRight, childBottom);
// 更新行宽
lineWidth = childWidth;
// 更新行高
lineHeight = childHeight;
// 下一个
continue;
}
// 不换行
// 布局
int childLeft = lineWidth;
int childTop = recordHeight + (recordHeight == 0 ? 0 : verticalSpace);
int childRight = lineWidth + childWidth;
int childBottom = recordHeight + childHeight + (recordHeight == 0 ? 0 : verticalSpace);
childView.layout(childLeft, childTop, childRight, childBottom);
// 更新行宽
lineWidth += childWidth;
// 更新行高,取最大
lineHeight = Math.max(childHeight, lineHeight);
}
}

只要实现以上3步,这个Layout就可以正常使用了,我们看看怎么添加元素。

适配器FlowAdapter

适配器用于把Object数据转化成View,然后交给FlowLayout显示。至于动态更新流式布局,也是操作这个适配器,采用观察者模式。FlowAdapter为发布者,FlowLayout为观察者,通过调用FlowAdapter的更新方法,便会通知FlowLayout观察者更新数据。我们看实现:

// FlowAdapter类
public abstract class FlowAdapter <T> implements FlowPublisher {

// 数据源
private List<T> dataList;
// 观察者
private FlowObserver observer;

public FlowAdapter(List<T> dataList) {
this.dataList = dataList;
}

@Override
public void register(FlowObserver observer) {
this.observer = observer;
}

@Override
public void unregister() {
this.observer = null;
}

/**
* 子类复写,由数据data获取View
* @param position
* @param data
* @return
*/

public abstract View getView(int position, T data);

/**
* 解析所有数据
* @return
*/

List<View> parseViews() {
List<View> viewList = new ArrayList<>();
for (int position = 0; position < dataList.size(); position++) {
viewList.add(getView(position, dataList.get(position)));
}
return viewList;
}

/**
* 通知更新
*/

public void notifyChange() {
if (observer != null) {
observer.notifyChange(this);
}
}

}

observer便是我们的FlowLayout,我们看看notifyChange方法:

// FlowLayout类
/**
* 观察者更新方法
* @param publisher
*/

@Override
public void notifyChange(FlowPublisher publisher) {
if (publisher instanceof FlowAdapter) {
refresh((FlowAdapter)publisher);
}
}
/**
* 刷新
*/

private void refresh(FlowAdapter adapter) {
// 移除所有子View
removeAllViews();
// 获取FlowAdapter所有子View
List<View> childViews = adapter.parseViews();
// 添加到FlowLayout中
for (int index = 0; index < childViews.size(); index++) {
final View childView = childViews.get(index);
// 设置点击事件
final int position = index;
childView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(position, childView);
}
}
});
// 添加
addView(childView);
}
// 更新
requestLayout();
}

很简单,就是移除所有子View,然后添加所有适配器的View。
当然还有setAdapter方法:

/**
* 设置适配器
* @param adapter
*/

public void setAdapter(FlowAdapter adapter) {
adapter.register(this);
refresh(adapter);
}

和ListView很像,有木有?!(虽然内部实现有点不一样)

使用实例

xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:flow_layout="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#9c9c9c">


<com.johan.library.viewtoolkit.flowlayout.FlowLayout
android:id="@+id/flow_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
flow_layout:horizontal_space="10dp"
flow_layout:vertical_space="10dp"
android:background="@android:color/white"
/>


<!-- 设置了精确的宽度 -->
<com.johan.library.viewtoolkit.flowlayout.FlowLayout
android:id="@+id/flow_layout2"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
flow_layout:horizontal_space="10dp"
flow_layout:vertical_space="10dp"
android:background="@android:color/white"
/>


</LinearLayout>

activity使用:

public class FlowLayoutActivity extends Activity {

private List<String> dataList = new ArrayList<>();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flow_layout);
dataList.add("Java");
dataList.add("Swift");
dataList.add("Html");
dataList.add("CSS");
dataList.add("Go");
dataList.add("C#");
dataList.add("PHP");
FlowLayout flowLayout = (FlowLayout) findViewById(R.id.flow_layout);
final FlowLayoutAdapter adapter = new FlowLayoutAdapter(dataList);
flowLayout.setAdapter(adapter);
flowLayout.setOnItemClickListener(new FlowLayout.OnItemClickListener() {
@Override
public void onItemClick(int position, View view) {
dataList.remove(position);
adapter.notifyChange();
}
});
FlowLayout flowLayout2 = (FlowLayout) findViewById(R.id.flow_layout2);
FlowLayoutAdapter adapter2 = new FlowLayoutAdapter(dataList);
flowLayout2.setAdapter(adapter2);
}

public class FlowLayoutAdapter extends FlowAdapter <String> {
public FlowLayoutAdapter(List<String> dataList) {
super(dataList);
}
@Override
public View getView(int position, String data) {
View layout = LayoutInflater.from(FlowLayoutActivity.this).inflate(R.layout.item_flow_layout, null);
TextView contentView = (TextView) layout.findViewById(R.id.item_content);
contentView.setText(data);
return layout;
}
}

}

效果图:

Android View - 实现流式布局

完整代码已经上传到我的github库,地址:https://github.com/JohanMan/viewtoolkit

参考资料

Android 手把手教您自定ViewGroup
Android 自定义ViewGroup 实战篇 -> 实现FlowLayout