Android实现RecyclerView中自定义树形节点的拖拽功能指南
嘿,我刚好做过类似的需求,给你梳理个入门级的实现思路,一步步来就能搞定~
从ListView树形视图迁移到支持拖拽的RecyclerView入门指南
一、先搞定树形数据的RecyclerView适配(核心基础)
RecyclerView是扁平化列表,所以得先把原来的树形结构转成一维列表来显示。首先定义一个清晰的节点数据类,用来存树形结构的关键信息:
data class TreeItem( val id: String, val title: String, var isExpanded: Boolean = false, // 展开/折叠状态 val children: MutableList<TreeItem> = mutableListOf(), // 子节点列表 var depth: Int = 0 // 用来控制节点缩进的层级深度 )
接下来写RecyclerView的Adapter,核心是维护一个扁平化的显示列表,并实现展开/折叠节点的逻辑:
class TreeAdapter(private val flatDisplayList: MutableList<TreeItem>) : RecyclerView.Adapter<TreeAdapter.TreeViewHolder>() { inner class TreeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val titleTv: TextView = itemView.findViewById(R.id.tv_tree_title) val expandIv: ImageView = itemView.findViewById(R.id.iv_expand_icon) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TreeViewHolder { val itemView = LayoutInflater.from(parent.context) .inflate(R.layout.item_tree_node, parent, false) return TreeViewHolder(itemView) } override fun onBindViewHolder(holder: TreeViewHolder, position: Int) { val currentItem = flatDisplayList[position] // 根据层级设置缩进,这里用padding快速实现 holder.titleTv.setPadding(currentItem.depth * 40, holder.titleTv.paddingTop, 0, holder.titleTv.paddingBottom) holder.titleTv.text = currentItem.title // 控制展开/折叠图标的显示与旋转 holder.expandIv.visibility = if (currentItem.children.isNotEmpty()) View.VISIBLE else View.GONE holder.expandIv.rotation = if (currentItem.isExpanded) 90f else 0f // 点击图标切换展开/折叠状态 holder.expandIv.setOnClickListener { currentItem.isExpanded = !currentItem.isExpanded if (currentItem.isExpanded) { expandNode(position) } else { collapseNode(position) } notifyDataSetChanged() // 入门级可以这么写,后续可优化为局部刷新 } } // 展开节点:把子节点插入到当前节点的下一个位置 private fun expandNode(position: Int) { val parentItem = flatDisplayList[position] var insertIndex = position + 1 parentItem.children.forEach { child -> child.depth = parentItem.depth + 1 flatDisplayList.add(insertIndex++, child) // 如果子节点本身是展开状态,递归展开 if (child.isExpanded) { expandNode(insertIndex - 1) } } } // 折叠节点:移除当前节点下所有层级更深的子节点 private fun collapseNode(position: Int) { val parentItem = flatDisplayList[position] var currentIndex = position + 1 while (currentIndex < flatDisplayList.size && flatDisplayList[currentIndex].depth > parentItem.depth) { val childItem = flatDisplayList[currentIndex] if (childItem.isExpanded) collapseNode(currentIndex) flatDisplayList.removeAt(currentIndex) } } override fun getItemCount() = flatDisplayList.size }
对应的item_tree_node.xml布局就做个简单的水平布局:左边放展开/折叠图标,右边放标题文本即可,这里就不贴代码啦~
二、添加节点拖拽功能(用官方ItemTouchHelper)
Android官方的ItemTouchHelper能快速实现拖拽排序,不用自己写复杂的触摸事件逻辑:
1. 实现拖拽回调类
class TreeDragCallback( private val adapter: TreeAdapter, private val onItemDrag: (fromPos: Int, toPos: Int) -> Boolean ) : ItemTouchHelper.Callback() { override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { // 只允许上下拖拽 val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN return makeMovementFlags(dragFlags, 0) // 不支持侧滑删除 } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { val fromPos = viewHolder.adapterPosition val toPos = target.adapterPosition return onItemDrag(fromPos, toPos) } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { // 不需要侧滑删除,空实现即可 } override fun isLongPressDragEnabled(): Boolean { return true // 允许长按触发拖拽 } // 可选:拖拽时给选中项加视觉反馈 override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { super.onSelectedChanged(viewHolder, actionState) if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { viewHolder?.itemView?.setBackgroundColor(Color.LTGRAY) } } override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder) viewHolder.itemView.setBackgroundColor(Color.TRANSPARENT) } }
2. 在页面中绑定RecyclerView和拖拽Helper
// 先构建测试用的树形数据 val rootNode1 = TreeItem("1", "根节点1") rootNode1.children.add(TreeItem("2", "子节点1-1")) rootNode1.children.add(TreeItem("3", "子节点1-2")) val rootNode2 = TreeItem("4", "根节点2") val flatList = mutableListOf(rootNode1, rootNode2) // 初始化RecyclerView和Adapter val recyclerView = findViewById<RecyclerView>(R.id.recycler_tree) recyclerView.layoutManager = LinearLayoutManager(this) val adapter = TreeAdapter(flatList) recyclerView.adapter = adapter // 绑定拖拽Helper val dragCallback = TreeDragCallback(adapter) { fromPos, toPos -> // 先处理扁平化列表的位置交换 val movedItem = adapter.flatDisplayList.removeAt(fromPos) adapter.flatDisplayList.add(toPos, movedItem) adapter.notifyItemMoved(fromPos, toPos) // !重要:这里需要同步更新原始的树形结构(比如把节点从原父节点移除,添加到新位置的父节点下) // 入门阶段可以先实现列表拖拽,后续再完善树形结构的同步逻辑 true } val itemTouchHelper = ItemTouchHelper(dragCallback) itemTouchHelper.attachToRecyclerView(recyclerView)
三、入门踩坑提示
- 树形结构与扁平化列表同步:拖拽后不能只更新显示列表,必须同步修改原始的树形数据结构,否则折叠/展开操作会出现数据错乱,这部分需要结合你的业务逻辑去完善。
- 性能优化:入门用
notifyDataSetChanged没问题,数据量大时建议换成notifyItemRangeInserted、notifyItemMoved等局部刷新方法。 - 缩进优化:用padding做缩进是最简便的方式,追求美观的话可以用自定义View或者MarginLayoutParams来实现更精细的缩进。
内容的提问来源于stack exchange,提问作者swaroop
相关产品推荐
相关产品推荐

