Launcher3的AllAppsContainerView类似实现:
RecyclerView + GridLayoutManager + SpanSizeLookup 实现自定义网格布局
核心组件功能
-
RecyclerView
- 高效展示大量数据的容器,支持
滚动和视图回收复用
- 高效展示大量数据的容器,支持
-
GridLayoutManager
- 提供网格布局能力,可设置列数(spanCount)和方向
-
GridLayoutManager.SpanSizeLookup
- 核心控制类,用于动态指定每个位置的Item应占据的列数
实现步骤与示例代码
1. 布局文件(activity_main.xml)
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="16dp"/>
2. 数据模型(ItemModel.java)
public class ItemModel {
private int type; // 视图类型
private String title; // 显示文本
public ItemModel(int type, String title) {
this.type = type;
this.title = title;
}
// Getters and Setters
public int getType() { return type; }
public String getTitle() { return title; }
}
3. 适配器(CustomGridAdapter.java)
public class CustomGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0; // 标题类型
private static final int TYPE_NORMAL = 1; // 普通项类型
private static final int TYPE_WIDE = 2; // 宽项类型
private List<ItemModel> itemList;
public CustomGridAdapter(List<ItemModel> itemList) {
this.itemList = itemList;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view;
if (viewType == TYPE_HEADER) {
view = inflater.inflate(R.layout.item_header, parent, false);
return new HeaderViewHolder(view);
} else if (viewType == TYPE_WIDE) {
view = inflater.inflate(R.layout.item_wide, parent, false);
return new WideViewHolder(view);
} else {
view = inflater.inflate(R.layout.item_normal, parent, false);
return new NormalViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ItemModel item = itemList.get(position);
if (holder instanceof HeaderViewHolder) {
((HeaderViewHolder) holder).textView.setText(item.getTitle());
} else if (holder instanceof WideViewHolder) {
((WideViewHolder) holder).textView.setText(item.getTitle());
} else {
((NormalViewHolder) holder).textView.setText(item.getTitle());
}
}
@Override
public int getItemCount() {
return itemList.size();
}
@Override
public int getItemViewType(int position) {
return itemList.get(position).getType();
}
// 定义ViewHolder类
static class HeaderViewHolder extends RecyclerView.ViewHolder {
TextView textView;
HeaderViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.header_text);
}
}
static class WideViewHolder extends RecyclerView.ViewHolder {
TextView textView;
WideViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.wide_text);
}
}
static class NormalViewHolder extends RecyclerView.ViewHolder {
TextView textView;
NormalViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.normal_text);
}
}
}
4. 自定义SpanSizeLookup(CustomSpanSizeLookup.java)
public class CustomSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
private CustomGridAdapter adapter;
private int totalSpans;
public CustomSpanSizeLookup(CustomGridAdapter adapter, int totalSpans) {
this.adapter = adapter;
this.totalSpans = totalSpans;
setSpanIndexCacheEnabled(true); // 启用缓存提高性能
}
@Override
public int getSpanSize(int position) {
int viewType = adapter.getItemViewType(position);
switch (viewType) {
case CustomGridAdapter.TYPE_HEADER:
return totalSpans; // 标题占据整行
case CustomGridAdapter.TYPE_WIDE:
return totalSpans / 2; // 宽项占据半行(假设总列数为4)
case CustomGridAdapter.TYPE_NORMAL:
default:
return 1; // 普通项占据1列
}
}
}
5. Activity中配置RecyclerView(MainActivity.java)
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
// 准备数据
List<ItemModel> itemList = prepareItems();
// 创建适配器
CustomGridAdapter adapter = new CustomGridAdapter(itemList);
// 设置GridLayoutManager,总列数为4
int spanCount = 4;
GridLayoutManager layoutManager = new GridLayoutManager(this, spanCount);
// 设置SpanSizeLookup
layoutManager.setSpanSizeLookup(new CustomSpanSizeLookup(adapter, spanCount));
// 配置RecyclerView
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
}
private List<ItemModel> prepareItems() {
List<ItemModel> items = new ArrayList<>();
// 添加标题
items.add(new ItemModel(CustomGridAdapter.TYPE_HEADER, "标题"));
// 添加普通项
for (int i = 1; i <= 8; i++) {
items.add(new ItemModel(CustomGridAdapter.TYPE_NORMAL, "普通项 " + i));
}
// 添加宽项
items.add(new ItemModel(CustomGridAdapter.TYPE_WIDE, "宽项"));
// 添加更多普通项
for (int i = 9; i <= 12; i++) {
items.add(new ItemModel(CustomGridAdapter.TYPE_NORMAL, "普通项 " + i));
}
return items;
}
}
关键概念解析
-
SpanSizeLookup的作用
getSpanSize(int position)
方法返回值决定了Item在网格中占据的列数- 返回值范围为1到
spanCount
(总列数) - 例如:总列数为4时,返回2表示该Item占据2列宽度
-
性能优化
setSpanIndexCacheEnabled(true)
启用缓存,避免重复计算相同位置的spanSize- 适合固定布局模式的列表,提升滚动流畅度
-
常见应用场景
- 标题行占据整行宽度
- 广告卡片占据半行或多行宽度
- 实现瀑布流布局(结合不同高度的Item)
效果示意图
总列数 = 4
+-----------------------------------+
| 标题 | (TYPE_HEADER, span=4)
+--------+--------+--------+--------+
| 普通项1 | 普通项2 | 普通项3 | 普通项4 | (TYPE_NORMAL, span=1)
+--------+--------+--------+--------+
| 普通项5 | 普通项6 | 普通项7 | 普通项8 |
+--------+--------+--------+--------+
| 宽项 | (TYPE_WIDE, span=2)
+--------+--------+--------+--------+
| 普通项9 | 普通项10| 普通项11| 普通项12|
+--------+--------+--------+--------+
注意事项
-
总列数(spanCount)选择
- 应根据设计需求和屏幕尺寸合理选择
- 手机端通常为2-4列,平板端可更多
-
视图类型与布局文件匹配
- 确保
getItemViewType
返回值与onCreateViewHolder
中的处理一致
- 确保
-
宽项尺寸适配
- 宽项的布局文件宽度应设置为
match_parent
以占满分配的空间
- 宽项的布局文件宽度应设置为