RecyclerView如何实现动态项单行折叠 点击行展示全部子项
问题根源
现有实现没有对同批次添加的清单项做分组标记,存储和渲染都是单条目平铺逻辑,自然会每个项单独占一行。要实现折叠展开效果,需要做两级列表结构+分组存储+展开状态控制三个核心改造。
分步改造方案
1. 改造数据层,增加批次分组字段
首先修改ChecklistHelper实体类,新增3个字段:
// 批次ID,同一次保存的所有清单项共用同一个batchId private long batchId; // 条目类型:1=分组头(折叠时显示的单行),0=普通子项 private int itemType; // 分组下的子项列表,仅分组头条目使用这个字段 private List<ChecklistHelper> childItems; // 自行补全对应字段的get/set方法
对应修改本地数据库的checklist表,新增batchId(长整型)、itemType(整型)两个字段。
2. 修改添加页的保存逻辑,生成分组
原来的保存逻辑有两个问题:一是ChecklistHelper在循环外初始化,所有条目会复用同一个对象导致状态错乱;二是没有给同批次条目打统一标记。修改AddChecklist的保存逻辑如下:
case R.id.btn_save: // 同批次条目共用同一个时间戳作为batchId,保证全局唯一 long currentBatchId = System.currentTimeMillis(); // 1. 先插入分组头记录 ChecklistHelper groupHeader = new ChecklistHelper(); groupHeader.setBatchId(currentBatchId); groupHeader.setItemType(1); groupHeader.setStatus("0"); // 分组头显示文本可以自定义,比如默认显示"新建清单项组(共X项)" groupHeader.setContent("新建清单项组(共" + linearLayout.getChildCount() + "项)"); dbHelper.insertChecklist(groupHeader.getStatus(), groupHeader.getContent(), DateTime.date(), DateTime.time(), currentBatchId, 1); // 2. 循环插入所有同批次子项 for (int i = 0; i < linearLayout.getChildCount(); i++) { View v = linearLayout.getChildAt(i); EditText etChecklistItem = v.findViewById(R.id.et_checklist_item); CheckBox checkBox = v.findViewById(R.id.check_box); ChecklistHelper childItem = new ChecklistHelper(); childItem.setBatchId(currentBatchId); childItem.setItemType(0); childItem.setStatus(checkBox.isChecked() ? "1" : "0"); childItem.setContent(etChecklistItem.getText().toString()); // 对应修改insertChecklist方法,新增batchId、itemType两个入参 dbHelper.insertChecklist(childItem.getStatus(), childItem.getContent(), DateTime.date(), DateTime.time(), currentBatchId, 0); } finish(); break;
注意:同步修改DbHelper的insertChecklist方法,新增的两个字段要写入数据库。
3. 改造列表页数据预处理,聚合分组数据
原来从数据库取出来的是所有平铺的记录,需要先聚合成分组结构,修改ChecklistItems页初始化数据的逻辑:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_checklist_items); recyclerView.setLayoutManager(new LinearLayoutManager(this)); dbHelper = new DbHelper(this); // 1. 取出所有原始记录 List<ChecklistHelper> rawList = dbHelper.getChecklists(); // 2. 聚合分组 List<ChecklistHelper> displayList = new ArrayList<>(); HashMap<Long, ChecklistHelper> groupMap = new HashMap<>(); for (ChecklistHelper item : rawList) { if (item.getItemType() == 1) { // 是分组头,初始化子项列表 item.setChildItems(new ArrayList<>()); groupMap.put(item.getBatchId(), item); displayList.add(item); } else { // 是子项,归到对应分组下 ChecklistHelper group = groupMap.get(item.getBatchId()); if (group != null) { group.getChildItems().add(item); } } } // 3. 把处理好的带分组结构的数据传给适配器 checklistAdapter = new ChecklistAdapter(displayList); recyclerView.setAdapter(checklistAdapter); }
4. 新增分组头布局,改造适配器支持折叠展开
首先新增分组头布局R.layout.checklist_layout_group,布局内放一个TextView显示组名、一个ImageView显示展开/折叠箭头即可。
然后改造ChecklistAdapter,支持多视图类型、展开状态控制:
public class ChecklistAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private boolean isEnableDelete = false; private final List<ChecklistHelper> displayList; // 记录每个分组的展开状态,key是分组的adapterPosition,value是是否展开 private final SparseArray<Boolean> expandStatus = new SparseArray<>(); private final int TYPE_GROUP = 1; private final int TYPE_CHILD = 0; public ChecklistAdapter(List<ChecklistHelper> displayList) { this.displayList = displayList; // 默认所有分组都是折叠状态 for (int i = 0; i < displayList.size(); i++) { expandStatus.put(i, false); } } @Override public int getItemViewType(int position) { return displayList.get(position).getItemType(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); if (viewType == TYPE_GROUP) { View groupView = layoutInflater.inflate(R.layout.checklist_layout_group, parent, false); return new GroupViewHolder(groupView); } else { View childView = layoutInflater.inflate(R.layout.checklist_layout_child, parent, false); return new ChecklistChildViewHolder(childView); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { ChecklistHelper currentItem = displayList.get(position); if (holder instanceof GroupViewHolder) { GroupViewHolder groupHolder = (GroupViewHolder) holder; groupHolder.tvGroupName.setText(currentItem.getContent()); // 根据展开状态切换箭头方向 boolean isExpanded = Boolean.TRUE.equals(expandStatus.get(position)); groupHolder.imgArrow.setRotation(isExpanded ? 90 : 0); // 点击分组头切换展开折叠 groupHolder.itemView.setOnClickListener(v -> { boolean currentExpand = Boolean.TRUE.equals(expandStatus.get(position)); expandStatus.put(position, !currentExpand); if (!currentExpand) { // 从折叠切展开:把该组所有子项插入到分组头后面 displayList.addAll(position + 1, currentItem.getChildItems()); notifyItemRangeInserted(position + 1, currentItem.getChildItems().size()); } else { // 从展开切折叠:移除该组所有子项 int childCount = currentItem.getChildItems().size(); for (int i = 0; i < childCount; i++) { displayList.remove(position + 1); } notifyItemRangeRemoved(position + 1, childCount); } notifyItemChanged(position); }); } else if (holder instanceof ChecklistChildViewHolder) { ChecklistChildViewHolder childHolder = (ChecklistChildViewHolder) holder; childHolder.updateVisibility(isEnableDelete); childHolder.checkBox.setText(currentItem.getContent()); // 处理勾选删除线逻辑 if ("1".equals(currentItem.getStatus())) { childHolder.checkBox.setChecked(true); childHolder.checkBox.setText(lineThrough(currentItem.getContent())); } else { childHolder.checkBox.setChecked(false); childHolder.checkBox.setText(currentItem.getContent()); } childHolder.checkBox.setOnCheckedChangeListener((compoundButton, b) -> { String txt = currentItem.getContent(); if (b) { childHolder.checkBox.setText(lineThrough(txt)); currentItem.setStatus("1"); } else { childHolder.checkBox.setText(txt); currentItem.setStatus("0"); } }); childHolder.delete.setOnClickListener(v -> { dbHelper.deleteChecklist(currentItem.getId()); // 删除后局部刷新即可,不用直接关闭页面 int removePos = holder.getAdapterPosition(); displayList.remove(removePos); notifyItemRemoved(removePos); }); } } @Override public int getItemCount() { return displayList.size(); } public void updateItems(boolean isEnableDelete){ this.isEnableDelete = isEnableDelete; notifyDataSetChanged(); } // 分组头ViewHolder public class GroupViewHolder extends RecyclerView.ViewHolder { TextView tvGroupName; ImageView imgArrow; public GroupViewHolder(@NonNull View itemView) { super(itemView); tvGroupName = itemView.findViewById(R.id.tv_group_name); imgArrow = itemView.findViewById(R.id.img_arrow); } } public class ChecklistChildViewHolder extends RecyclerView.ViewHolder { CheckBox checkBox; ImageView delete, edit; ChecklistChildViewHolder(@NonNull View itemView) { super(itemView); checkBox = itemView.findViewById(R.id.checkbox_child); delete = itemView.findViewById(R.id.delete_child); edit = itemView.findViewById(R.id.edit_child); delete.setVisibility(View.INVISIBLE); edit.setVisibility(View.INVISIBLE); } public void updateVisibility(boolean isEnableDelete){ if (isEnableDelete) { delete.setVisibility(View.VISIBLE); edit.setVisibility(View.VISIBLE); } else { delete.setVisibility(View.INVISIBLE); edit.setVisibility(View.INVISIBLE); } } } }
补充优化点
- 可以给展开折叠加上RecyclerView默认的ItemAnimator,交互更自然
- 分组头的显示文本可以自定义,比如取第一个子项的内容加“等X项”的后缀
- 数据库查询的时候可以按batchId排序,避免分组顺序错乱
- 编辑模式下的子项修改逻辑,可以在折叠时同步更新对应分组头的显示文本
内容的提问来源于stack exchange,提问作者nano tech
相关产品推荐
相关产品推荐

