自定义view实现历史记录流式布局
在 Android 开发中,流式布局是一种常见的 UI 设计模式,尤其适用于展示动态内容,如标签、历史记录等。本文将介绍如何通过自定义 View 来实现一个历史记录的流式布局。
使用自定义view实现历史记录流式布局的运行结果图:
1. 创建自定义 ViewGroup
首先,我们需要创建一个名为 FlowLayout
的类,继承自 ViewGroup
。在构造函数中,我们可以初始化一些必要的属性。
public class FlowLayout extends ViewGroup {
private List<List<View>> allListView = new ArrayList<>(); // 所有View的二维数组
private List<Integer> allListHeight = new ArrayList<>(); // 每一行的高度
private int mywidthspacing = 16; // 宽间距
private int myheightspacing = 8; // 高间距
public FlowLayout(Context context) {
super(context);
} // new
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
} // 反射
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}// xml
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
2. 重写 onMeasure
方法
在 onMeasure
方法中,我们需要测量每个子视图的大小,并计算出 FlowLayout
的最终宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调用父类的 onMeasure 方法,确保父类的测量逻辑被执行
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 初始化存储所有行的视图和高度的列表,防止 onMeasure 被多次调用时出现数据错误
allListView = new ArrayList<>();
allListHeight = new ArrayList<>();
// 获取父视图的宽度和高度
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 初始化总高度和总宽度
int allheight = 0;
int allwidth = 0;
// 获取子视图的数量
int count = getChildCount();
// 初始化当前行的宽度和高度
int linewidthuser = 0;
int lineHeight = 0;
// 用于存储当前行的视图
List<View> listview = new ArrayList<>();
// 遍历所有子视图
for (int i = 0; i < count; i++) {
View childView = getChildAt(i); // 获取当前子视图
LayoutParams childLP = childView.getLayoutParams(); // 获取子视图的布局参数
// 如果子视图可见
if (childView.getVisibility() != GONE) {
// 计算子视图的测量规格
int childwidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingRight(), childLP.width);
int childheightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLP.height);
// 测量子视图的宽高
childView.measure(childwidthMeasureSpec, childheightMeasureSpec);
int childMeasuredWidth = childView.getMeasuredWidth(); // 获取测量后的宽度
int childMeasuredHeight = childView.getMeasuredHeight(); // 获取测量后的高度
// 判断当前行是否能放下下一个子视图,特别需要注意加入当前view的Padding的距离
if (linewidthuser + childMeasuredWidth + mywidthspacing > widthSize - getPaddingLeft() - getPaddingRight()) {
// 如果放不下,保存当前行的视图和高度
allListView.add(listview);
allListHeight.add(lineHeight);
// 更新总宽度和总高度
allwidth = Math.max(allwidth, linewidthuser + mywidthspacing);
allheight = allheight + lineHeight + myheightspacing;
// 换行,重置当前行的宽高
linewidthuser = 0;
lineHeight = 0;
listview = new ArrayList<>();
// 将当前子视图作为新行的第一个视图
listview.add(childView);
linewidthuser += childMeasuredWidth + mywidthspacing; // 更新当前行宽度
lineHeight = Math.max(childMeasuredHeight, lineHeight); // 更新当前行高度
} else {
// 如果可以放下,继续添加到当前行
listview.add(childView);
linewidthuser += childMeasuredWidth + mywidthspacing; // 更新当前行宽度
lineHeight = Math.max(childMeasuredHeight, lineHeight); // 更新当前行高度
// 如果是第一行,更新总宽度和高度
if (allListView.size() < 1) {
allheight = Math.max(childMeasuredHeight, allheight);
allwidth = linewidthuser;
}
}
// 如果是最后一个子视图,保存当前行的视图和高度
if (i == count - 1) {
allListView.add(listview);
allListHeight.add(childMeasuredHeight);
}
}
}
// 获取测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 根据测量模式来获取需要的宽度和高度
int realWidthSize = (widthMode == MeasureSpec.EXACTLY) ? widthSize : allwidth + getPaddingLeft() + getPaddingRight();
int realHeightSize = (heightMode == MeasureSpec.EXACTLY) ? heightSize : allheight + getPaddingBottom() + getPaddingTop();
// 设置测量后的宽高
setMeasuredDimension(realWidthSize, realHeightSize);
}
3. 重写 onLayout
方法
在 onLayout
方法中,我们需要根据测量的结果来布局每个子视图的位置。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取当前视图的左、上、右、下边距
int curLeft = getPaddingLeft(); // 当前行的起始左边距
int curTop = getPaddingTop(); // 当前行的起始上边距
// 遍历所有行
for (int i = 0; i < allListView.size(); i++) { // 遍历每一行
curLeft = getPaddingLeft(); // 每行开始时重置左边距
// 遍历当前行的所有视图
for (int j = 0; j < allListView.get(i).size(); j++) { // 遍历当前行的每个视图
View view = allListView.get(i).get(j); // 获取当前视图
// 定义视图的布局边界
int left, top, right, bottom;
left = curLeft; // 当前视图的左边界
top = curTop; // 当前视图的上边界
right = left + view.getMeasuredWidth(); // 当前视图的右边界
bottom = top + view.getMeasuredHeight(); // 当前视图的下边界
// 布局当前视图
view.layout(left, top, right, bottom);
// 更新当前行的左边距,为下一个视图留出空间
curLeft = right + mywidthspacing; // 加上视图的宽度和水平间距
}
// 更新当前行的顶部边距,为下一行留出空间
curTop = curTop + allListHeight.get(i) + myheightspacing; // 加上当前行的高度和垂直间距
// 重置左边距,以便下一行的布局
curLeft = getPaddingLeft(); // 每行开始时重置左边距
}
}
4.使用
在布局文件中,注意把com.example.note换成自己的包名:
<com.example.note.FlowLayout
android:id="@+id/flowLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />
这里就自己加的几个数据在Activity中运行测试:
public class TestActivity extends AppCompatActivity {
private FlowLayout flowLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
flowLayout = findViewById(R.id.flowLayout);
ArrayList<String> list = new ArrayList<>();
list.add("你好");
list.add("我是谁");
list.add("历史记录");
list.add("搜索我的物品A");
list.add("搜索我的物品B");
list.add("搜索我的物品C");
list.add("搜索我的物品D");
list.add("你好呀");
list.add("自定义View");
list.add("流式布局实现啦");
// 添加文字到 FlowLayout
for (int i = 0; i < list.size(); i++) {
final TextView textView = new TextView(this);
textView.setText(list.get(i));
GradientDrawable drawable = new GradientDrawable();
drawable.setShape(GradientDrawable.RECTANGLE);
drawable.setColor(Color.GRAY); // 设置背景颜色为灰色
drawable.setCornerRadius(26f); // 设置圆角半径
textView.setBackground(drawable);
textView.setTextSize(20f);
textView.setTextColor(Color.WHITE); // 设置字体颜色为白色
textView.setPadding(16, 8, 16, 8); // 设置内边距
textView.setOnClickListener(v -> {
// 处理按钮点击事件
});
flowLayout.addView(textView);
}
}
}