先看效果

原理说明
使用recyclerView 动态添加、删除Item,实现显示效果。recyclerView本身就支持view的复用,所以用它来实现树形控件的性能是非常好的。
总体思路
既然是控件,就要适用于各种各样的数据。所以我们要泛型编程。首先要抽象数据,使我们的控件能适配各种各样的数据。然后将数据构造成树的结构。最后使用recyclerView动态进行添加和删除。思路看起来有点粗略,下面一起来实现吧。
代码实现
抽象数据
设计一种既能适配各种数据类型,又能满足树形控件要求的实体类。想一想,既然是树形控件,那么树的节点一定会有一个id和parentId。又要兼容各种数据类型,所以,我们可以考虑把id和parentId做成接口,具体数据类型做成泛型。
设想一下,一个树形控件的模型(树的节点)需要哪些字段?
1、有id,pid,用户数据T
2、树的节点应该有层级的吧 每个层级前面的缩进是不一样的 所以给它一个 level字段
3、树的节点应该有一个字段表明树是否展开吧 所以给他一个 isExpand字段
4、树的节点可能有子节点的吧 所以给他一个List children字段
5、既然有子节点,那也应该有父节点吧 所以给他一个 parent字段
6、取个名字吧 叫 TreeNode
public interface NodeId {
public String getId();
public String getPId();
}
// 泛型参数应该要实现NodeId接口,确保能拿到id和ParentId
public class TreeNode<T extends NodeId> {
private final static String TAG = "TreeNode";
private T data; // 用户的数据
private int level; // 层级
private boolean isExpand; // 是否展开
private TreeNode<T> parent; // 父节点
private List<TreeNode<T>> 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<TreeNode<T>> getChildren() {
return children;
}
public void setChildren(List<TreeNode<T>> 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 static <T extends NodeId> List<TreeNode<T>> convertDataToTreeNode(List<T> datas) {
List<TreeNode<T>> nodes = new ArrayList<>();
Map<String, TreeNode<T>> map = new HashMap();
for (NodeId nodeId : datas) {
TreeNode treeNode = new TreeNode(nodeId);
nodes.add(treeNode);
map.put(nodeId.getId(), treeNode);
}
Iterator<TreeNode<T>> iterator = nodes.iterator();
while(iterator.hasNext()){
TreeNode<T> treeNode = iterator.next();
String pId = treeNode.getPId();
TreeNode<T> parentNode = map.get(pId);
if (parentNode != null) {
parentNode.getChildren().add(treeNode);
treeNode.setParent(parentNode);
iterator.remove();
}
}
return nodes;
}
同时,我们还需要一个方法,当结点展开或者关闭的时候,我们需要获取结点下面的子结点,动态的添加或者删除。有人就问了,不是可以通过treeNode.getChildren()方法直接获取吗? 这样只对了一半。因为如果我仅仅通过treeNode.getChildren()获取子节点,只能获取到该结点下的子节点,却不能获取子结点的子节点。所有我们要递归去获取。
public static <T extends NodeId> List<TreeNode<T>> getNodeChildren(TreeNode<T> node) {
List<TreeNode<T>> result = new ArrayList<>();
getRNodeChildren(result, node);
return result;
}
private static <T extends NodeId> void getRNodeChildren(List<TreeNode<T>> result, TreeNode<T> node) {
List<TreeNode<T>> children = node.getChildren();
for (TreeNode n : children) {
result.add(n);
if (n.isExpand() && !n.isLeaf()) {
getRNodeChildren(result, n);
}
}
}
现在方法有了,数据结构也有了。下面就交给recycerView了。
RecyclerView实现动态添加删除结点
推荐一个比较好用的recycler适配器,这次的TreeView就是基于这个适配器的。
项目地址是 github.com/CymChad/Bas…
思考一下,需要在适配器里面做点什么?
其实适配器和适配普通RecyclerView差不多,唯一我们需要多做的就是添加一个点击事件,当点击结点的时候,判断一下当前状态,如果当前是关闭,那么就展开,并且添加子结点到数据源。
同时应该在初始化item的时候根据level为item设置一个padding距离,这样能让树形控件看起来具有层级关系。
public class SingleLayoutTreeAdapter<T extends NodeId> extends BaseQuickAdapter<TreeNode<T>, BaseViewHolder> {
public interface OnTreeClickedListener<T extends NodeId> {
void onNodeClicked(View view, TreeNode<T> node, int position);
void onLeafClicked(View view, TreeNode<T> node, int position);
}
private OnTreeClickedListener onTreeClickedListener;
public SingleLayoutTreeAdapter(int layoutResId, @Nullable final List<TreeNode<T>> dataToBind) {
super(layoutResId, dataToBind);
setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
TreeNode<T> node = dataToBind.get(position);
if (!node.isLeaf()) {
List<TreeNode<T>> 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<T> 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);
}
}
完整的代码实现在 github.com/ZhangHao555…