前段时间项目里需要大量使用树形结构的控件,由于开发周期的关系,第一时间去GitHub找了Star最多的库AndroidTreeView进行改造,该控件的原理很简单:View的结构和树形结构一致,每一个节点的View为一个LinearLayout,包含两个子View,第一个为自身,第二个为包裹子节点的ViewGroup。第一次显示TreeView的时候默认展开一级(当然也可以用expandAll方法展开全部),展开的逻辑就是遍历所有子节点,然后拿到ViewHolder进行添加,每一个节点持有一个ViewHolder,ViewHolder包含有View和渲染View的方法,第一次展开的时候根据ViewHolder渲染的方法创建出View,下次展开的时候就不用new了。
好了,分析完这个控件之后来看看其中的优缺点,优点:结构简单,思路清晰,构造数据方便。缺点:性能差!性能差!性能差!当子节点数量达到50就能感觉到展开有明显卡顿,这一个缺点足以让我放弃了该控件,恰好这期迭代安排有变动,有一周时间用来重写这个部分。
性能不好当然得用性能好的方式解决,RecyclerView就是我们的主角,把所有节点都视为RecyclerView的一个Item,这样就能做到无论树的节点数量多大,深度有多深,都能回收利用ItemView。经过一周的改造和优化,目前该控件基本能满足一般场景的树形控件需求。 先看效果: 
实现详解
先看基本类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);
}
//其他方法省略
}
树节点的基本样子,没啥好说的,只是要注意addChild的时候不光要add,还得顺便把child的parent赋值。接下来看TreeView类:
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);
//省略部分代码
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();
}
//其他方法省略
}
这是TreeView的直接操作类,外部的一切动作都通过TreeView转接。TreeView的创建必须传入一个BaseNodeViewFactory,这个工厂用来根据level得到每一级的BaseNodeViewBinder,稍后会分析这个类,然后创建一个RecyclerView作为rootView。TreeView包含了展开、收起、选择等全部的方法,但他只负责转接,具体实现细节全交给了TreeHelper和Adapter,那我们就先进入主角Adapter:
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);
//省略部分代码
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();
}
Adapter中用一个expandedNodeList存储当前可见的节点(包含屏幕外的节点),所以每次刷新之前都得重新计算这个集合,buildExpandedNodeList在首次或者变动较大的时候才用到,其他情况刷新局部数据会节省很多开销。接着分析adapter的两个关键方法onCreateViewHolder和onBindViewHolder:
onCreateViewHolder中根据BaseNodeViewBinder拿到layoutId,生成布局构BaseNodeViewBinder,这里是比较关键的部分,构造BaseNodeViewBinder必须传入View和Level,而View是用nodeViewBinder.getLayoutId()生成的,也就是要用的参数需要从结果中拿到,这里就形成了矛盾。仔细分析一下,这里我们只关注怎么拿到layoutId,只要成功构造出BaseNodeViewBinder完成任务,即使传入任意View也没有任何影响,因为真正的View是拿到layoutId之后生成的View,所以这里我们就传入了一个非空的new View(context)。
onBindViewHolder承担了部分bind的职责,负责处理展开收起点击事件和选择逻辑,其他的细节由使用者实现。这里简单分析点击展开的逻辑,展开一个节点分为两步:首先拿到展开后新增需要显示的数据,接着调用notifyItemRangeInserted刷新局部。关键在于第一步,这一步的计算是在TreeHelper中进行,之前的TreeView中也多次用到这个类,TreeHelper其实就是纯负责计算的类,展开收起,添加删除,选择计算,脏活累活全交给了它,里面的具体算法细节就不多分析,感兴趣可以自行查看。
还有一个和Adapter紧密相关的BaseNodeViewBinder类,代码如下:
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
}
}
BaseNodeViewBinder继承自ViewHolder,该类不光是一个ViewHolder,还包含了CreateViewHolder(拿到layoutId,具体过程还是在Adapter中进行),BindViewHolder的职责,可能你会觉得职责不够单一,但作为使用者来说清晰和简单就够了,因为使用者只用关心我的每一层级的节点布局是哪个、我该怎么把数据绑定到节点上,具体你这个类内部在干啥我根本不关注。另外还有两个不是必须实现的方法,getToggleTriggerViewId用来指定你想要点击触发展开收起操作的View,如果不指定默认是点击全部区域,当展开收起之后你需要做其他事就实现onNodeToggled方法。BaseNodeViewBinder还有一个子类CheckableNodeViewBinder,如果需要用到选择功能实现它就好了。
如何使用
添加依赖
compile 'me.texy.treeview:treeview_lib:1.0.1'
实现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
.
.
.
如果需要用选择功能则继承自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;
}
}
}
生成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