手把手教你实现高性能Android树形控件 TreeView

6,946 阅读5分钟

先看效果

原理说明

使用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…