先看效果
原理说明
使用recyclerView 动态添加、删除Item,实现显示效果。recyclerView本身就支持view的复用,所以用它来实现树形控件的性能是非常好的。
总体思路
既然是控件,就要适用于各种各样的数据。所以我们要泛型编程。首先要抽象数据,使我们的控件能适配各种各样的数据。然后将数据构造成树的结构。最后使用recyclerView动态进行添加和删除。思路看起来有点粗略,下面一起来实现吧。
代码实现
抽象数据
设计一种既能适配各种数据类型,又能满足树形控件要求的实体类。想一想,既然是树形控件,那么树的节点一定会有一个id和parentId。又要兼容各种数据类型,所以,我们可以考虑把id和parentId做成接口,具体数据类型做成泛型。
设想一下,一个树形控件的模型(树的节点)需要哪些字段?
1、有id,pid,用户数据T 2、树的节点应该有层级的吧 每个层级前面的缩进是不一样的 所以给它一个 level字段 3、树的节点应该有一个字段表明树是否展开吧 所以给他一个 isExpand字段 4、树的节点可能有子节点的吧 所以给他一个List children字段 5、既然有子节点,那也应该有父节点吧 所以给他一个 parent字段 6、取个名字吧 叫 TreeNodepublic interface NodeId { public String getId(); public String getPId();}复制代码
// 泛型参数应该要实现NodeId接口,确保能拿到id和ParentIdpublic class TreeNode{ private final static String TAG = "TreeNode"; private T data; // 用户的数据 private int level; // 层级 private boolean isExpand; // 是否展开 private TreeNode parent; // 父节点 private List > children = new ArrayList<>(); // 孩子结点 public TreeNode(T data) { this(data,-1); } public TreeNode(T data) { this.data = data; } public String getId() { if (data == null) { Log.e(TAG, "getId: data is null"); return ""; } return data.getId(); } public String getPId() { if (data == null) { Log.e(TAG, "getParentId: data is null"); return ""; } return data.getPId(); } // 如果没有父节点,就证明是跟结点 public boolean isRoot() { return parent == null; } public boolean isParentExpand() { if (parent == null) { return false; } return parent.isExpand(); } public boolean isExpand() { return isExpand; } // 设置结点关闭的时候,因该将改结点下的所有结点一起关闭。 public void setExpand(boolean expand) { isExpand = expand; if (!isExpand) { for (TreeNode node : children) { node.setExpand(false); } } } public int getLevel() { return parent == null ? 0 : parent.getLevel() + 1; } public T getData() { return data; } public void setData(T data) { this.data = data; } public void setLevel(int level) { this.level = level; } public TreeNode getParent() { return parent; } public void setParent(TreeNode parent) { this.parent = parent; } public List > getChildren() { return children; } public void setChildren(List > children) { this.children = children; } public boolean isLeaf() { return ListUtil.isEmpty(children); //是否是叶子结点,没有子结点,就证明是叶子结点 }}复制代码
这样,我们就将用户的数据全部转化为TreeNode类型,然后使用TreeNode作为树形控件的模型统一操作。
构造树形结构
树结点的数据模型有了,下一步就是将数据构造成树形的结构。
写一个Util方法,它的作用就是 将用户的数据构造成TreeNode,并设置好treeNode里面的各种属性。最重要的就是设置好parent和Children属性。 算法有多种,这里说一下我的思路。 根据用户数据构造出TreeNode,然后将TreeNode放入一个map中,以Id为key,treeNode为value,最后再次遍历TreeNode,在map中根据pid找到它的父节点。public staticList > convertDataToTreeNode(List datas) { List > nodes = new ArrayList<>(); Map > map = new HashMap(); for (NodeId nodeId : datas) { TreeNode treeNode = new TreeNode(nodeId); nodes.add(treeNode); map.put(nodeId.getId(), treeNode); } Iterator > iterator = nodes.iterator(); while(iterator.hasNext()){ TreeNode treeNode = iterator.next(); String pId = treeNode.getPId(); TreeNode parentNode = map.get(pId); if (parentNode != null) { parentNode.getChildren().add(treeNode); treeNode.setParent(parentNode); iterator.remove(); } } return nodes; }复制代码
同时,我们还需要一个方法,当结点展开或者关闭的时候,我们需要获取结点下面的子结点,动态的添加或者删除。有人就问了,不是可以通过treeNode.getChildren()方法直接获取吗? 这样只对了一半。因为如果我仅仅通过treeNode.getChildren()获取子节点,只能获取到该结点下的子节点,却不能获取子结点的子节点。所有我们要递归去获取。
public staticList > getNodeChildren(TreeNode node) { List > result = new ArrayList<>(); getRNodeChildren(result, node); return result; } private static void getRNodeChildren(List > result, TreeNode node) { List > children = node.getChildren(); for (TreeNode n : children) { result.add(n); if (n.isExpand() && !n.isLeaf()) { getRNodeChildren(result, n); } } }复制代码
现在方法有了,数据结构也有了。下面就交给recycerView了。
RecyclerView实现动态添加删除结点
推荐一个比较好用的recycler适配器,这次的TreeView就是基于这个适配器的。
项目地址是思考一下,需要在适配器里面做点什么?
其实适配器和适配普通RecyclerView差不多,唯一我们需要多做的就是添加一个点击事件,当点击结点的时候,判断一下当前状态,如果当前是关闭,那么就展开,并且添加子结点到数据源。 同时应该在初始化item的时候根据level为item设置一个padding距离,这样能让树形控件看起来具有层级关系。public class SingleLayoutTreeAdapterextends BaseQuickAdapter , BaseViewHolder> { public interface OnTreeClickedListener { void onNodeClicked(View view, TreeNode node, int position); void onLeafClicked(View view, TreeNode node, int position); } private OnTreeClickedListener onTreeClickedListener; public SingleLayoutTreeAdapter(int layoutResId, @Nullable final List > dataToBind) { super(layoutResId, dataToBind); setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(BaseQuickAdapter adapter, View view, int position) { TreeNode node = dataToBind.get(position); if (!node.isLeaf()) { List > children = TreeDataUtils.getNodeChildren(node); if (node.isExpand()) { dataToBind.removeAll(children); node.setExpand(false); notifyItemRangeRemoved(position + 1, children.size()); } else { dataToBind.addAll(position + 1, children); node.setExpand(true); notifyItemRangeInserted(position + 1, children.size()); } if (onTreeClickedListener != null) { onTreeClickedListener.onNodeClicked(view, node, position); } } else { if (onTreeClickedListener != null) { onTreeClickedListener.onLeafClicked(view, node, position); } } } }); } @Override protected void convert(BaseViewHolder helper, TreeNode item) { int level = item.getLevel(); ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) helper.itemView.getLayoutParams(); layoutParams.leftMargin = getTreeNodeMargin() * level; } public void setOnTreeClickedListener(OnTreeClickedListener onTreeClickedListener) { this.onTreeClickedListener = onTreeClickedListener; } protected int getTreeNodeMargin() { return DpUtil.dip2px(this.mContext, 10); }}复制代码
完整的代码实现在