一、功能介绍
- 本文基于 Android 打造任意层级树形控件 考验你的数据结构和设计- 的实现
- 树形无限层级列表-RecyclerView实现
- 支持递归更新选中状态
- 支持递归计算文件大小
- 支持递归删除
二、实现原理介绍
树形可展开的布局主要是使用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适配器的实现
- 这里我们就可以看到2个数据了:
存储所有可见的Node: mNodes
存储所有的Node:mAllNodes
当我们点击展开和隐藏的时候。 mNodes = TreeHelper.filterVisibleNode(mAllNodes) 会调整哪些node需要可见,哪些需要隐藏 - 基于递归,实现选中的联动变化 TreeHelper.setNodeChecked(mNodes[position], isChecked)
- 基于递归,实现删除按钮 deleteSelectedNode()
- 基于递归,实现文件大小变化 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)
}
}
}
其实实现的文件特别少,树形数据结构,递归思想很多。
下载