多级可展开的树形结构的Recyclerview

一、功能介绍

  1. 本文基于 Android 打造任意层级树形控件 考验你的数据结构和设计- https://blog.csdn.net/lmj623565791/article/details/40212367 的实现

  2. 树形无限层级列表-RecyclerView实现

  3. 支持递归更新选中状态

  4. 支持递归计算文件大小

  5. 支持递归删除
    在这里插入图片描述

二、实现原理介绍

树形可展开的布局主要是使用2个数据维护,一个是供RecycleView显示的mNodes,一个是保存有完整数据mAllNodes。

即用户看到视图都是来源于mNodes。例如被展开的数据会被添加到mNodes,被隐藏的数据会被移除出mNodes。

三、编码实现

3.1 主界面布局

主界面很简单就一个RecyclerView和删除的Button

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	android:orientation="vertical">
	<androidx.recyclerview.widget.RecyclerView
		android:id="@+id/tree_lv"
		android:layout_width="match_parent"
	    android:layout_height="0dp"
		app:layout_constraintVertical_weight="1"
		app:layout_constraintStart_toStartOf="parent"
		app:layout_constraintEnd_toEndOf="parent"
		app:layout_constraintTop_toTopOf="parent"
		app:layout_constraintBottom_toTopOf="@+id/del_bt"/>

	<Button
		android:id="@+id/del_bt"
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:layout_margin="16dp"
		android:text="删除"
		app:layout_constraintStart_toStartOf="parent"
		app:layout_constraintEnd_toEndOf="parent"
		app:layout_constraintTop_toBottomOf="@+id/tree_lv"
		app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

3.2 主界面的逻辑实现

填充了模拟数据、初始化RecyclerView并添加对于的适配器、按钮的删除功能调用,还有RecyclerView的点击事件监听。都是非常传统的操作,尽量简单处理。这里需要重点关注 TreeListViewAdapter

package com.sufadi.treelistviewdemo

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity(), TreeListViewAdapter.OnTreeNodeClickListener {
    private lateinit var adapter: TreeListViewAdapter
    private val mNodeList: MutableList<Node> = ArrayList<Node>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initNodeList()
        initRecyclerView()
        initButton()
    }

    private fun initNodeList() {
        var node: Node
        mNodeList.add(Node(1, 0, "音乐目录"))
        mNodeList.add(Node(2, 1, "周杰伦"))
        mNodeList.add(Node(3, 1, "林俊杰"))

        node = Node(4, 2, "晴天.mp3")
        node.size = 16
        mNodeList.add(node)

        node = Node(5, 3, "被风吹过的夏天.mp3")
        node.size = 20
        mNodeList.add(node)

        mNodeList.add(Node(6, 0, "视频目录"))
        mNodeList.add(Node(7, 6, "国产"))
        mNodeList.add(Node(8, 6, "欧美"))

        node = Node(9, 7, "大闹天宫.mp4")
        node.size = 1000
        mNodeList.add(node)

        node = Node(10, 7, "哪吒闹海.mp4")
        node.size = 2000
        mNodeList.add(node)

        node = Node(11, 7, "西游记.mp4")
        node.size = 3000
        mNodeList.add(node)

        node = Node(12, 8, "泰坦尼克号.mp4")
        node.size = 3500
        mNodeList.add(node)

        mNodeList.add(Node(13, 7, "港台"))

        node = Node(14, 13, "无间道1.mp4")
        node.size = 1500
        mNodeList.add(node)

        node = Node(15, 13, "无间道2.mp4")
        node.size = 1600
        mNodeList.add(node)

        node = Node(16, 13, "无间道3.mp4")
        node.size = 1700
        mNodeList.add(node)
    }

    private fun initRecyclerView() {
        adapter = TreeListViewAdapter(mNodeList)
        adapter.setOnTreeNodeClickListener(this)
        tree_lv.layoutManager = LinearLayoutManager(this)
        tree_lv.adapter = adapter
    }

    private fun initButton() {
        del_bt.setOnClickListener {
            adapter.deleteSelectedNode()
        }
    }

    override fun onTreeItemClick(node: Node, position: Int) {
        if (node.isLeaf) {
            Toast.makeText(applicationContext, node.name,
                Toast.LENGTH_SHORT).show()
        }
    }
}

3.3 RecyclerView适配器的实现

  1. 这里我们就可以看到2个数据了:
    存储所有可见的Node: mNodes
    存储所有的Node:mAllNodes
    当我们点击展开和隐藏的时候。 mNodes = TreeHelper.filterVisibleNode(mAllNodes) 会调整哪些node需要可见,哪些需要隐藏
  2. 基于递归,实现选中的联动变化 TreeHelper.setNodeChecked(mNodes[position], isChecked)
  3. 基于递归,实现删除按钮 deleteSelectedNode()
  4. 基于递归,实现文件大小变化 mNodes[position].getNoteSize()

其实本文章最核心的是TreeHelper和Node

package com.sufadi.treelistviewdemo

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView

/**
 * tree适配器
 */
class TreeListViewAdapter(nodeList: MutableList<Node>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    /**
     * 存储所有可见的Node
     */
    private var mNodes: MutableList<Node>

    /**
     * 存储所有的Node
     */
    private var mAllNodes: MutableList<Node> = TreeHelper.getSortedNodes(nodeList)

    /**
     * TreeList item的点击的回调接口
     */
    private var onTreeNodeClickListener: OnTreeNodeClickListener? = null

    interface OnTreeNodeClickListener {
        fun onTreeItemClick(node: Node, position: Int)
    }

    fun setOnTreeNodeClickListener(onTreeNodeClickListener: OnTreeNodeClickListener?) {
        this.onTreeNodeClickListener = onTreeNodeClickListener
    }

    init {
        /**
         * 过滤出可见的Node
         */
        mNodes = TreeHelper.filterVisibleNode(mAllNodes)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
        view.setPadding(viewType * 60, 3, 3, 3)
        return InnerViewHolder(view)
    }

    class InnerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val itemLayout = itemView.findViewById<ConstraintLayout>(R.id.item_layout)
        val icon = itemView.findViewById<ImageView>(R.id.id_treenode_icon)
        val checkBox = itemView.findViewById<CheckBox>(R.id.id_treeNode_check)
        val name = itemView.findViewById<TextView>(R.id.id_treenode_name)
        val size = itemView.findViewById<TextView>(R.id.size_tv)
    }

    override fun getItemViewType(position: Int): Int {
        return mNodes[position].level
    }

    override fun getItemCount(): Int {
        return mNodes.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val innerViewHolder = holder as InnerViewHolder
        innerViewHolder.icon.visibility = View.VISIBLE
        if(mNodes[position].isExpand && mNodes[position].childrenNodes.size > 0) {
            innerViewHolder.icon.setImageResource(R.mipmap.ic_arrow_up)
        } else if(!mNodes[position].isExpand && mNodes[position].childrenNodes.size > 0) {
            innerViewHolder.icon.setImageResource(R.mipmap.ic_arrow_down)
        } else {
            innerViewHolder.icon.visibility = View.GONE
        }

        innerViewHolder.checkBox.isChecked = mNodes[position].isChecked
        innerViewHolder.name.text = mNodes[position].name

        innerViewHolder.checkBox.setOnClickListener {
            val isChecked = !mNodes[position].isChecked
            TreeHelper.setNodeChecked(mNodes[position], isChecked)
            notifyDataSetChanged()
        }
        innerViewHolder.itemLayout.setOnClickListener {
            expandOrCollapse(position)
            onTreeNodeClickListener?.onTreeItemClick(mNodes[position], position)
        }
        innerViewHolder.size.text = mNodes[position].getNoteSize().toString() + " MB"
    }

    /**
     * 相应ListView的点击事件 展开或关闭某节点
     */
    private fun expandOrCollapse(position: Int) {
        val n = mNodes[position]
        n?.let {
            if (!n.isLeaf) {
                n.isExpand = !n.isExpand
                mNodes = TreeHelper.filterVisibleNode(mAllNodes)
                notifyDataSetChanged()
            }
        }
    }

    fun deleteSelectedNode() {
        val selectedIdList = getSelectedNodeListId()
        deleteNode(selectedIdList, mAllNodes)
        updateNodeList(selectedIdList, mAllNodes)
        deleteNode(selectedIdList, mNodes)
        updateNodeList(selectedIdList, mNodes)
        notifyDataSetChanged()
    }

    private fun getSelectedNodeListId(): MutableList<Int> {
        val selectedList:MutableList<Int> = mutableListOf()
        for (node in mAllNodes) {
            if (node.isChecked) {
                if (!selectedList.contains(node.id)) selectedList.add(node.id)
            }
        }
        return selectedList
    }

    private fun deleteNode(selectedNodeListId: MutableList<Int>, nodeList: MutableList<Node>) {
        val iterator = nodeList.iterator()
        while (iterator.hasNext()) {
            val node = iterator.next()
            if (selectedNodeListId.contains(node.id)) {
                iterator.remove()
            }
        }
    }

    private fun updateNodeList(selectedIdList: MutableList<Int>, nodeList : MutableList<Node>) {
        for (node in nodeList) {
            deleteUnusedNodeInfo(selectedIdList, node)
        }
    }

    /**
     * 删除操作,更新每个节点中已被删除的信息
     */
    private fun deleteUnusedNodeInfo(selectedIdList: MutableList<Int>, node: Node) {
        val childrenIterator = node.childrenNodes.iterator()
        while (childrenIterator.hasNext()) {
            val delChildrenNode = childrenIterator.next()
            if (selectedIdList.contains(delChildrenNode.id)) {
                childrenIterator.remove()
            }
            if (!delChildrenNode.isLeaf) {
                deleteUnusedNodeInfo(selectedIdList, delChildrenNode)
            }
        }
    }
}

还有个adapter布局文件也公布下

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_layout"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"
    android:minHeight="58dip">

    <TextView
        android:id="@+id/id_treenode_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/id_treenode_icon"
        android:text="@string/app_name"
        android:textSize="18sp" />

    <ImageView
        android:id="@+id/id_treenode_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@+id/id_treenode_name"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:src="@mipmap/ic_arrow_down"/>

    <TextView
        android:id="@+id/size_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/id_treeNode_check" />

    <CheckBox
        android:id="@+id/id_treeNode_check"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:focusable="false" />

</androidx.constraintlayout.widget.ConstraintLayout>

3.4 核心类 Node

关键看节点id,父节点id,是否展开,节点级别

package com.sufadi.treelistviewdemo;

import java.util.ArrayList;
import java.util.List;

public class Node {
	/**
	 * 节点id
	 */
	private int id;
	/**
	 * 父节点id
	 */
	private int parentId;
	/**
	 * 是否展开
	 */
	private boolean isExpand = true;
	private boolean isChecked = false;
	private boolean isHideChecked = true;
	/**
	 * 节点名字
	 */
	private String name;
	/**
	 * 节点级别
	 */
	private int level;
	/**
	 * 节点展示图标
	 */
	private int icon;
	/**
	 * 节点所含的子节点
	 */
	private List<Node> childrenNodes = new ArrayList<Node>();
	/**
	 * 节点的父节点
	 */
	private Node parent;

	public Node() {
	}

	public Node(int id, int parentId, String name) {
		super();
		this.id = id;
		this.parentId = parentId;
		this.name = name;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public int getparentId() {
		return parentId;
	}

	public void setparentId(int parentId) {
		this.parentId = parentId;
	}

	public boolean isExpand() {
		return isExpand;
	}

	/**
	 * 当父节点收起,其子节点也收起
	 * @param isExpand
	 */
	public void setExpand(boolean isExpand) {
		this.isExpand = isExpand;
		if (!isExpand) {
			for (Node node : childrenNodes) {
				node.setExpand(isExpand);
			}
		}
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getLevel() {
		return parent == null ? 0 : parent.getLevel() + 1;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public int getIcon() {
		return icon;
	}

	public void setIcon(int icon) {
		this.icon = icon;
	}

	public List<Node> getChildrenNodes() {
		return childrenNodes;
	}

	public void setChildrenNodes(List<Node> childrenNodes) {
		this.childrenNodes = childrenNodes;
	}

	public Node getParent() {
		return parent;
	}

	public void setParent(Node parent) {
		this.parent = parent;
	}

	/**
	 * 判断是否是根节点
	 * 
	 * @return
	 */
	public boolean isRoot() {
		return parent == null;
	}

	/**
	 * 判断是否是叶子节点
	 * 
	 * @return
	 */
	public boolean isLeaf() {
		return childrenNodes.size() == 0;
	}
	

	/**
	 * 判断父节点是否展开
	 */
	public boolean isParentExpand() {
		if (parent == null)
			return false;
		return parent.isExpand();
	}

	public boolean isChecked() {
		return isChecked;
	}

	public void setChecked(boolean isChecked) {
		this.isChecked = isChecked;
	}

	public boolean isHideChecked() {
		return isHideChecked;
	}

	public long size = 0L;

	public Long getNoteSize() {
		return getNoteSize(this);
	}

	public Long getNoteSize(Node node){
		 if (node.isLeaf()) {
			 return node.size;
		} else {
			 return getChildrenNodeSize(node.childrenNodes);
		}
	}

	private Long getChildrenNodeSize(List<Node> childrenList) {
		long countSize = 0L;
		for (Node node : childrenList) {
			if (node.isLeaf()) {
				countSize += node.size;
			} else {
				countSize += getChildrenNodeSize(node.childrenNodes);
			}
		}
		return countSize;
	}

	@Override
	public String toString() {
		return "Node{" +
				"id=" + id +
				", parentId=" + parentId +
				", isExpand=" + isExpand +
				", isChecked=" + isChecked +
				", isHideChecked=" + isHideChecked +
				", name='" + name + '\'' +
				", level=" + level +
				", icon=" + icon +
				", size=" + size +
				'}';
	}
}

3.5 核心类 TreeHelper

实现了很多实用和核心的工具类方法

package com.sufadi.treelistviewdemo

import kotlin.collections.ArrayList

object TreeHelper {
    /**
     * 根据所有节点获取可见节点
     */
    fun filterVisibleNode(allNodes: MutableList<Node>): MutableList<Node> {
        val visibleNodes: MutableList<Node> = ArrayList()
        for (node in allNodes) {
            // 如果为根节点,或者上层目录为展开状态
            if (node.isRoot || node.isParentExpand) {
                visibleNodes.add(node)
            }
        }
        return visibleNodes
    }

    /**
     * 获取排序的所有节点
     */
    fun getSortedNodes(nodeList: MutableList<Node>): MutableList<Node> {
        val sortedNodes: MutableList<Node> = ArrayList()
        val nodes = buildTreeNodes(nodeList)
        // 拿到根节点
        val rootNodes = getRootNodes(nodes)
        // 排序以及设置Node间关系
        for (node in rootNodes) {
            addNode(sortedNodes, node, 1)
        }
        return sortedNodes
    }

    /**
     * 把一个节点上的所有的内容都挂上去
     */
    private fun addNode(nodes: MutableList<Node>, node: Node, currentLevel: Int) {
        nodes.add(node)
        if (node.isLeaf) return
        for (i in node.childrenNodes.indices) {
            addNode(nodes, node.childrenNodes[i], currentLevel + 1)
        }
    }

    /**
     * 获取所有的根节点
     */
    private fun getRootNodes(nodes: MutableList<Node>): MutableList<Node> {
        val rootNodes: MutableList<Node> = ArrayList()
        for (node in nodes) {
            if (node.isRoot) {
                rootNodes.add(node)
            }
        }
        return rootNodes
    }

    /**
     * 将Node的parent和children的数据填充
     */
    private fun buildTreeNodes(nodes: MutableList<Node>): MutableList<Node> {
        /**
         * 比较nodes中的所有节点,分别添加children和parent
         */
        for (i in nodes.indices) {
            val n = nodes[i]
            for (j in i + 1 until nodes.size) {
                val m = nodes[j]
                if (n.id == m.getparentId()) {
                    n.childrenNodes.add(m)
                    m.parent = n
                } else if (n.getparentId() == m.id) {
                    n.parent = m
                    m.childrenNodes.add(n)
                }
            }
        }
        return nodes
    }



    fun setNodeChecked(node: Node, isChecked: Boolean) {
        // 自己设置是否选择
        node.isChecked = isChecked
        /**
         * 非叶子节点,子节点处理
         */
        setChildrenNodeChecked(node, isChecked)
        /** 父节点处理  */
        setParentNodeChecked(node)
    }

    /**
     * 非叶子节点,子节点处理
     */
    private fun setChildrenNodeChecked(node: Node, isChecked: Boolean) {
        node.isChecked = isChecked
        if (!node.isLeaf) {
            for (n in node.childrenNodes) {
                // 所有子节点设置是否选择
                setChildrenNodeChecked(n, isChecked)
            }
        }
    }

    /**
     * 设置父节点选择
     *
     * @param node
     */
    private fun setParentNodeChecked(node: Node) {
        /** 非根节点  */
        if (!node.isRoot) {
            val rootNode = node.parent
            var isAllChecked = true
            for (n in rootNode.childrenNodes) {
                if (!n.isChecked) {
                    isAllChecked = false
                    break
                }
            }
            rootNode.isChecked = isAllChecked
            setParentNodeChecked(rootNode)
        }
    }
}

其实实现的文件特别少,树形数据结构,递归思想很多。

下载

https://download.csdn.net/download/su749520/19848461

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

法迪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值