最近在做的项目决定用流式布局来展示历史记录,刚好自己也想学习自定义 ViewGroup,所以就参考了其他的一些文章,写了一个 FlowLayout(流式布局),效果如下:
只有一个 FlowLayout 类,完整代码如下:
/**
* @author Feng Zhaohao
* Created on 2019/11/10
*/
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private List<Rect> mChildrenPositionList = new ArrayList<>(); // 记录各子 View 的位置
private int mMaxLines = Integer.MAX_VALUE; // 最多显示的行数,默认无限制
private int mVisibleItemCount; // 可见的 item 数
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 清除之前的位置
mChildrenPositionList.clear();
// 测量所有子元素(这样 child.getMeasuredXXX 才能获取到值)
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int[] a = helper(widthSize);
int measuredHeight = 0;
// EXACTLY 模式:对应指定大小和 match_parent
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
}
// AT_MOST 模式,对应 wrap_content
else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = a[0];
}
int measuredWidth = 0;
if (widthMode == MeasureSpec.EXACTLY) {
measuredWidth = widthSize;
}
else if (widthMode == MeasureSpec.AT_MOST) {
measuredWidth = a[1];
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
* 在 wrap_content 情况下,得到布局的测量高度和测量宽度
* 返回值是一个有两个元素的数组 a,a[0] 代表测量高度,a[1] 代表测量宽度
*/
private int[] helper(int widthSize) {
boolean isOneRow = true; // 是否是单行
int width = getPaddingLeft(); // 记录当前行已有的宽度
int height = getPaddingTop(); // 记录当前行已有的高度
int maxHeight = 0; // 记录当前行的最大高度
int currLine = 1; // 记录当前行数
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 获取当前子元素的 margin
LayoutParams params = child.getLayoutParams();
MarginLayoutParams mlp;
if (params instanceof MarginLayoutParams) {
mlp = (MarginLayoutParams) params;
} else {
mlp = new MarginLayoutParams(params);
}
// 记录子元素所占宽度和高度
int childWidth = mlp.leftMargin + child.getMeasuredWidth() + mlp.rightMargin;
int childHeight = mlp.topMargin + child.getMeasuredHeight() + mlp.bottomMargin;
maxHeight = Math.max(maxHeight, childHeight);
// 判断是否要换行
if (width + childWidth + getPaddingRight() > widthSize) {
// 加上该行的最大高度
height += maxHeight;
// 重置 width 和 maxHeight
width = getPaddingLeft();
maxHeight = childHeight;
isOneRow = false;
currLine++;
if (currLine > mMaxLines) {
break;
}
}
// 存储该子元素的位置,在 onLayout 时设置
Rect rect = new Rect(width + mlp.leftMargin,
height + mlp.topMargin,
width + childWidth - mlp.rightMargin,
height + childHeight - mlp.bottomMargin);
mChildrenPositionList.add(rect);
// 加上该子元素的宽度
width += childWidth;
}
int[] res = new int[2];
res[0] = height + maxHeight + getPaddingBottom();
res[1] = isOneRow? width + getPaddingRight() : widthSize;
return res;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 布置子 View 的位置
int n = Math.min(getChildCount(), mChildrenPositionList.size());
for (int i = 0; i < n; i++) {
View child = getChildAt(i);
Rect rect = mChildrenPositionList.get(i);
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
mVisibleItemCount = n;
}
/**
* 设置 Adapter
*/
public void setAdapter(Adapter adapter) {
// 移除之前的视图
removeAllViews();
// 添加 item
int n = adapter.getItemCount();
for (int i = 0; i < n; i++) {
ViewHolder holder = adapter.onCreateViewHolder(this);
adapter.onBindViewHolder(holder, i);
View child = holder.itemView;
addView(child);
}
}
/**
* 设置最多显示的行数
*/
public void setMaxLines(int maxLines) {
mMaxLines = maxLines;
}
/**
* 获取显示的 item 数
*/
public int getVisibleItemCount() {
return mVisibleItemCount;
}
public abstract static class Adapter<VH extends ViewHolder> {
public abstract VH onCreateViewHolder(ViewGroup parent);
public abstract void onBindViewHolder(VH holder, int position);
public abstract int getItemCount();
}
public abstract static class ViewHolder {
public final View itemView;
public ViewHolder(View itemView) {
if (itemView == null) {
throw new IllegalArgumentException("itemView may not be null");
}
this.itemView = itemView;
}
}
}
详细说明请看注释,这里就不多说了。下面看下使用方法:
使用方法和 RecyclerView 类似,先编写 item 的布局,然后编写 Adapter,最后给 FlowLayout 设置 Adapter 即可显示数据。
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_light"
android:textColor="@android:color/white"
android:textSize="24sp"
android:id="@+id/tv_test_content"
android:padding="10dp" />
简单起见,布局只有一个 TextView。
注意:不能在 xml 中设置 margin 属性,需在代码中设置,后面会说明。
public class FlowAdapter extends FlowLayout.Adapter<FlowAdapter.FlowViewHolder> {
private static final String TAG = "FlowAdapter";
private Context mContext;
private List<String> mContentList;
public FlowAdapter(Context mContext, List<String> mContentList) {
this.mContext = mContext;
this.mContentList = mContentList;
}
@Override
public FlowViewHolder onCreateViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_test, parent, false);
// 给 View 设置 margin
ViewGroup.MarginLayoutParams mlp = new ViewGroup.MarginLayoutParams(view.getLayoutParams());
int margin = BaseUtil.dip2px(mContext, 5);
mlp.setMargins(margin, margin, margin, margin);
view.setLayoutParams(mlp);
return new FlowViewHolder(view);
}
@Override
public void onBindViewHolder(FlowViewHolder holder, int position) {
holder.content.setText(mContentList.get(position));
}
@Override
public int getItemCount() {
return mContentList.size();
}
class FlowViewHolder extends FlowLayout.ViewHolder {
TextView content;
public FlowViewHolder(View itemView) {
super(itemView);
content = itemView.findViewById(R.id.tv_test_content);
}
}
}
如果想要给子 View 设置 margin,需在 onCreateViewHolder
方法创建 View 时设置:
// 给 View 设置 margin
ViewGroup.MarginLayoutParams mlp = new ViewGroup.MarginLayoutParams(view.getLayoutParams());
int margin = BaseUtil.dip2px(mContext, 5);
mlp.setMargins(margin, margin, margin, margin);
view.setLayoutParams(mlp);
private static final String TAG = "TestActivity";
private FlowLayout mFlowLayout;
private List<String> mContentList = new ArrayList<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initData();
initView();
}
private void initData() {
for (int i = 0; i < 4; i++) {
mContentList.add("java");
mContentList.add("kotlin");
mContentList.add("android");
mContentList.add("android-studio");
mContentList.add("app");
}
}
@Override
protected void initView() {
mFlowLayout = findViewById(R.id.fv_test_flow_layout);
// 设置 Adapter
FlowAdapter adapter = new FlowAdapter(this, mContentList);
mFlowLayout.setAdapter(adapter);
// 设置最多显示的行数
mFlowLayout.setMaxLines(3);
// 获取显示的 item 数
mFlowLayout.post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "count = " + mFlowLayout.getVisibleItemCount());
}
});
}