You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:20:22