Some time ago, the project needs to use a large number of tree structure controls, due to the relationship of the development cycle, the first time to GitHub to find the most Star library AndroidTreeView for transformation, the principle of the control is very simple :View structure and tree structure consistent, The View of each node is a LinearLayout that contains two sub-views, the first is itself and the second is the ViewGroup that wraps the sub-nodes. The first time the TreeView is displayed, the default expansion is one level (or expandAll). The logic of the expansion is to iterate through all the children and then take the ViewHolder to add them. Each node holds a ViewHolder. The ViewHolder contains the View and the methods to render the View. The View is created the first time the ViewHolder renders the View.

Well, after the analysis of this control to see the advantages and disadvantages, advantages: simple structure, clear thinking, convenient construction of data. Cons: Poor performance! Poor performance! Poor performance! When the number of child nodes reached 50, I could feel the obvious lag in the expansion, which was enough to make me give up the control. Just as the iteration schedule changed, I had a week to rewrite this part.

If the performance is not good, of course, it has to be solved in a good way. RecyclerView is our leading role, and all nodes are regarded as an Item of RecyclerView, so that no matter how large the number of nodes in the tree is, how deep the depth is, ItemView can be recycled. After a week of transformation and optimization, the control can basically meet the general scene of the tree control requirements. First look at the effect:

Implementation,

Let’s start with the base class TreeNode:

public class TreeNode { private int level; private Object value; private TreeNode parent; private List\<TreeNode\> children; private int index; private boolean expanded; private boolean selected; private boolean itemClickEnable = true; public TreeNode(Object value) { this.value = value; this.children = new ArrayList\<\>(); } public static TreeNode root() { TreeNode treeNode = new TreeNode(null); return treeNode; } public void addChild(TreeNode treeNode) { if (treeNode == null) { return; } children.add(treeNode); treeNode.setIndex(getChildren().size()); treeNode.setParent(this); } // other methods omit}Copy the code

There is nothing to say about the basic appearance of the tree node, except that when you add child, you must also assign the parent value of the child. Next, the TreeView class:

public class TreeView implements SelectableTreeAction { private TreeNode root; private Context context; private BaseNodeViewFactory baseNodeViewFactory; private RecyclerView rootView; private TreeViewAdapter adapter; Public TreeView(@nonnull TreeNode root, @NonNull Context context,@NonNull BaseNodeViewFactory baseNodeViewFactory) { this.root = root; this.context = context; this.baseNodeViewFactory = baseNodeViewFactory; if (baseNodeViewFactory == null) { throw new IllegalArgumentException("You must assign a BaseNodeViewFactory!" ); } } public View getView() { if (rootView == null) { this.rootView = buildRootView(); } return rootView; } @NonNull private RecyclerView buildRootView() { RecyclerView recyclerView = new RecyclerView(context); / / omit part of the code recyclerView. SetLayoutManager (new LinearLayoutManager (context)); adapter = new TreeViewAdapter(context, root, baseNodeViewFactory); recyclerView.setAdapter(adapter); return recyclerView; } @Override public void expandAll() { if (root == null) { return; } TreeHelper.expandAll(root); refreshTreeView(); } private void refreshTreeView() { if (rootView ! = null) { ((TreeViewAdapter) rootView.getAdapter()).refreshView(); } } @Override public void expandNode(TreeNode treeNode) { adapter.expandNode(treeNode); } @Override public void expandLevel(int level) { TreeHelper.expandLevel(root, level); refreshTreeView(); } // other methods omit}Copy the code

This is the TreeView’s direct action class, and all external actions are routed through the TreeView. The TreeView must be created by passing in a BaseNodeViewFactory, which is used to get the BaseNodeViewBinder for each level based on the level. We will analyze this class later and then create a RecyclerView as a rootView. The TreeView contains all the methods for expanding, folding, selecting, and so on, but it is only responsible for transferring. The implementation details are left to the TreeHelper and Adapter. Let’s go to the main Adapter first:

public class TreeViewAdapter extends RecyclerView.Adapter { private Context context; private TreeNode root; private List\<TreeNode\> expandedNodeList; private BaseNodeViewFactory baseNodeViewFactory; private View EMPTY_PARAMETER; public TreeViewAdapter(Context context, TreeNode root, @NonNull BaseNodeViewFactory baseNodeViewFactory) { this.context = context; this.root = root; this.baseNodeViewFactory = baseNodeViewFactory; this.EMPTY_PARAMETER = new View(context); this.expandedNodeList = new ArrayList\<\>(); buildExpandedNodeList(); } private void buildExpandedNodeList() { expandedNodeList.clear(); for (TreeNode child : root.getChildren()) { insertNode(expandedNodeList, child); } } private void insertNode(List\<TreeNode\> nodeList, TreeNode treeNode) { nodeList.add(treeNode); if (! treeNode.hasChild()) { return; } if (treeNode.isExpanded()) { for (TreeNode child : treeNode.getChildren()) { insertNode(nodeList, child); } } } @Override public int getItemViewType(int position) { return expandedNodeList.get(position).getLevel(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int level) { View view = LayoutInflater.from(context).inflate(baseNodeViewFactory .getNodeViewBinder(EMPTY_PARAMETER, level).getLayoutId(), parent, false); return baseNodeViewFactory.getNodeViewBinder(view, level); } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { final View nodeView = holder.itemView; final TreeNode treeNode = expandedNodeList.get(position); final BaseNodeViewBinder viewBinder = getNodeBinder(treeNode); / / omit part of the code if (viewBinder getToggleTriggerViewId ()! = 0) { View triggerToggleView = nodeView.findViewById(viewBinder.getToggleTriggerViewId()); if (triggerToggleView ! = null) { triggerToggleView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onNodeToggled(treeNode); viewBinder.onNodeToggled(nodeView, treeNode, treeNode.isExpanded()); }}); } } viewBinder.bindView(nodeView, treeNode); } private void onNodeToggled(TreeNode treeNode) { treeNode.setExpanded(! treeNode.isExpanded()); if (treeNode.isExpanded()) { expandNode(treeNode); } else { collapseNode(treeNode); } } public void expandNode(TreeNode treeNode) { if (treeNode == null) { return; } List\<TreeNode\> additionNodes = TreeHelper.expandNode(treeNode, false); int index = expandedNodeList.indexOf(treeNode); insertNodesAtIndex(index, additionNodes); } public void collapseNode(TreeNode treeNode) { if (treeNode == null) { return; } List\<TreeNode\> removedNodes = TreeHelper.collapseNode(treeNode, false); int index = expandedNodeList.indexOf(treeNode); removeNodesAtIndex(index, removedNodes); @override public int getItemCount() {return expandedNodeList == null? 0 : expandedNodeList.size(); }Copy the code

Adapter uses an expandedNodeList to store the currently visible nodes (including off-screen nodes), so the set has to be recalculated before each refresh. BuildExpandedNodeList is only used for the first time or for large changes. In other cases, refreshing local data can save a lot of overhead. Then analyze the two key adapter methods onCreateViewHolder and onBindViewHolder:

According to get layoutId BaseNodeViewBinder onCreateViewHolder, generate layout structure BaseNodeViewBinder, here is the key part, construct BaseNodeViewBinder must pass in the View and Level, . But the View is to use nodeViewBinder getLayoutId (generation), also is to use the parameters of the need to get from the results, there formed a contradiction. So just to be a little bit more specific, we’re just going to focus on getting layoutId, and once we’ve successfully constructed BaseNodeViewBinder to do that, it doesn’t matter if we pass in any View, because the real View is the View that’s generated when we get layoutId, So here we’re passing in a non-empty new View(Context).

The onBindViewHolder takes over some of bind’s responsibilities, handling the unfolding click event and selection logic, and the rest of the details are handled by the user. To expand a node, there are two steps: get the expanded data to be displayed, and call notifyItemRangeInserted to refresh the section. The key is the first step, this step is calculated in the TreeHelper, the previous TreeView has also used this class many times, TreeHelper is actually pure responsible for the calculation of the class, expand and fold up, add and delete, select the calculation, dirty work all handed over to it, inside the specific algorithm details are not much analysis, You can check it out if you are interested.

There is also a BaseNodeViewBinder class closely related to Adapter, with the following code:

public abstract class BaseNodeViewBinder extends RecyclerView.ViewHolder {  
  
    public BaseNodeViewBinder(View itemView) {  
        super(itemView);  
    }  
  
    public abstract int getLayoutId();
  
    public abstract void bindView(View view, TreeNode treeNode);  
  
    public int getToggleTriggerViewId() {  
        return 0;  
    }  
  
    public void onNodeToggled(View item, TreeNode treeNode, boolean expand) {  
        //empty  
    }  
}Copy the code

BaseNodeViewBinder inherits from ViewHolder. This class is not only a ViewHolder, but also contains the responsibilities of CreateViewHolder, You may feel that the responsibility is not simple enough, but as a user, it is clear and simple enough, because the user only cares about my node layout at each level and how I bind the data to the node, and I don’t care about what is going on inside your class. GetToggleTriggerViewId is used to specify which View you want to click on to trigger the expand and collapse operation. If this is not specified, the default is to click on all areas. Once you have expanded and collapsed, you implement the onNodeToggled method when you need to do something else. BaseNodeViewBinder also has a subclass, CheckableNodeViewBinder, which is good if you need to implement selection.

How to use

Add the dependent

The compile 'me. Texy. Treeview: treeview_lib: 1.0.1'Copy the code

Implement BaseNodeViewBinder

Sample:

public class FirstLevelNodeViewBinder extends BaseNodeViewBinder {  
    public FirstLevelNodeViewBinder(View itemView) {  
        super(itemView);  
    }  
  
    @Override  
    public int getLayoutId() {  
        return R.layout.item_first_level;  
    }  
  
    @Override  
    public void bindView(View view, final TreeNode treeNode) {  
        TextView textView = (TextView) view.findViewById(R.id.node_name_view);  
        textView.setText(treeNode.getValue().toString());  
    }
}

SecondLevelNodeViewBinder
ThirdLevelNodeViewBinder
.
.
.Copy the code

If you need to use the selection function inherit from CheckableNodeViewBinder BaseNodeViewFactory

Sample:

public class MyNodeViewFactory extends BaseNodeViewFactory { @Override public BaseNodeViewBinder getNodeViewBinder(View view, int level) { switch (level) { case 0: return new FirstLevelNodeViewBinder(view); case 1: return new SecondLevelNodeViewBinder(view); case 2: return new ThirdLevelNodeViewBinder(view); default: return null; }}}Copy the code

Generate the TreeView

Sample:

TreeNode root = TreeNode.root();
//build the tree as you want
for (int i = 0; i \< 5; i++) {  
    TreeNode treeNode = new TreeNode(new String("Child " + "No." + i));  
    treeNode.setLevel(0);  
    root.addChild(treeNode);  
}
View treeView = new TreeView(root, context, new MyNodeViewFactory()).getView();
//add to view group where you want Copy the code

If you want to learn more details or adapt to use, you can download this project at GitHub and also welcome to Issue.