您的位置:首页 > 移动开发 > Android开发

android 基于树结构的任意层级列表

2016-03-18 16:34 190 查看
最近写了一个基于树结构的任意层级列表,其展现效果就类似于android原生的ExpandableListView啦,不过自己写一个也是挺好玩的。在这里我主要是使用到了树这种数据结构来实现任意层级可展开/收起的效果的。先来看一下效果图呗。







嗯...看起来比较朴素。。嘛,android的listView你随便丢上来也很朴素的...这个不是关键嘛,视图这种东西你想要多华丽都可以后续去搞嘛,做个demo不要那么抠细节~~~

好的,说正事哈。在介绍怎么实现这种层级列表之前,我们先来看看树这个东西是怎么用的哈。



嗯...到百度上借了一张图,简单的讲一下,这样一棵树对应到层级列表上就是这个样子的:

A

---B

------D

---C

------E

这是一个三级列表。

那么,像上面那三张效果图中的那种层级列表对应到树又是怎么样的呢?看下面一张图。



嗯...用keynote随手画的,将就着看吧。其实了解树这一数据结构的孩子应该对比着看了以后心里就一目了然了。列表全部展开以后其各个item显示的顺序就是这棵树深度优先遍历的顺序。不了解的孩子可以先百度一下~~~百度百科也够用了。

嗯...所以总结一句话,要实现任意层级的列表,必须先实现如何构造一颗任意高度和宽度的树,以及如何对该树进行深度优先遍历。

1. 构造一棵树的准备

首先,树得有节点,我们很容易就会想到,树作为一个对象,其包含的元素主要就是节点,节点也作为一个对象,因此,我们需要构造一个节点的类。那么,我们进一步考虑,这个节点类要做哪些工作呢?比较基本的,它得有一个对自身的标识吧,也就是id,作为保证能在树中找到它的唯一标识;其次,它也得有孩子吧,那么它得有个保持其孩子引用的集合;再者,它也可以保有一个其父节点的引用。针对这些基本元素,节点类也要有一些接口来操作吧,例如添加孩子节点,获取节点内容,id等,还有获取该节点在列表中的层级,也就是其在树中的高度。有孩子可能会奇怪,为啥要获取其高度呢?嗯...这个在后面你们就会看到了。

/**
* ExpandableTreeListView的数据类
* Created by Ivan on 16/2/13.
*/
public class TreeListNodeData<T> {

private int parentId;
private int id;
private int level;
private T contents;

/**
* 层级列表中树的一个节点
* @param pid 父节点的id
* @param id 该节点的id
* @param contents 该节点所代表的内容,其用于显示到列表中
*/
public TreeListNodeData(int pid, int id, T contents) {
this.id = id;
this.parentId = pid;
this.contents = contents;
}

public int getParentId() {
return parentId;
}

public int getId() {
return id;
}

public T getContents() {
return contents;
}

/**
* 获取该节点的层级
* @return 该节点在列表中的层级,即其在树中的高度
*/
public int getLevel() {
return level;
}

protected void setLevel(int level) {
this.level = level;
}
}


/**
* 树的节点
* Created by Ivan on 16/2/13.
*/
class TreeNode {

private TreeListNodeData contents;
private TreeNode parentNode;
private ArrayList<TreeNode> childrenNodeList;
private boolean expand = false;

public TreeNode(TreeListNodeData contents) {
setContents(contents);
}

public TreeListNodeData getContents() {
return contents;
}

public TreeNode getParentNode() {
return parentNode;
}

public ArrayList<TreeNode> getChildrenNodeList() {
return childrenNodeList;
}

public void setContents(TreeListNodeData contents) {
this.contents = contents;
}

public void setParentNode(TreeNode parentNode) {
this.parentNode = parentNode;
}

public void expand() {
this.expand = true;
}

public void close() {
this.expand = false;
}

public boolean isExpand() {
return expand;
}

public int getId() {
return contents.getId();
}

public int getParentId() {
return contents.getParentId();
}

public void addChild(TreeNode child) {
if (childrenNodeList == null) {
childrenNodeList = new ArrayList<>();
}
childrenNodeList.add(child);
child.setParentNode(this);
child.setLevel(getLevel()+1);
}

protected int getLevel() {
return contents.getLevel();
}

protected void setLevel(int level) {
contents.setLevel(level);
}

}


2. 如何构造一棵树

这个问题其实没啥好说的,就是根据调用者提供的数据一直给对应的节点addChild而已...

首先,我们需要在节点类的基础上构造一个树类,用来对构造树,遍历树,寻找树节点进行操作。

/**
* 树数据结构类
* Created by Ivan on 16/2/13.
*/
public class Tree {

private TreeNode root;

public Tree(TreeListNodeData root) {
setRoot(root);
}

public void setRoot(TreeListNodeData root) {
this.root = new TreeNode(root);
this.root.expand();
root.setLevel(0);
}

public TreeNode getRoot() {
return root;
}

public void addChild(TreeNode parent, TreeNode child) {
parent.addChild(child);
}

/**
* 深度优先搜索获得树的节点有序列表
* @return 按照深度优先排序的节点列表
*/
public ArrayList<TreeNode> searchDepthFirst() {
ArrayList<TreeNode> results = new ArrayList<>();
results.add(root);
searchDepthFirst(root, results);
return results;
}

private void searchDepthFirst(TreeNode node, ArrayList<TreeNode> collection) {
if (!node.isExpand()) {
return;
}
ArrayList<TreeNode> children = node.getChildrenNodeList();
if (children == null) {
return;
}
for (int i=0;i<children.size();i++) {
TreeNode child = children.get(i);
collection.add(child);
searchDepthFirst(child, collection);
}
}

/**
* 获取指定id的节点
* @param id 指定节点的id
* @return 树节点
*/
public TreeNode findNode(int id) {
Holder holder = new Holder();
find(id, root, holder);
return holder.node;
}

private void find(int id, TreeNode node, Holder holder) {
if (node.getId() == id) {
holder.node = node;
return;
}
ArrayList<TreeNode> children = node.getChildrenNodeList();
if (children == null) {
return;
}
for (int i=0;i<children.size();i++) {
TreeNode child = children.get(i);
find(id, child, holder);
if (holder.node != null) {
return;
}
}
}

private class Holder {
TreeNode node;
}
}
下面给出构造一棵树的代码。我们可以看到,代码是根据一个装有TreeListNodeData数据的ArrayList构造出一棵树的。

private Tree generateTree(ArrayList<TreeListNodeData> listData) {
Tree tree = new Tree(listData.get(0));

for (int i=1;i<listData.size();i++) {
TreeNode child = new TreeNode(listData.get(i));
TreeNode parent = tree.findNode(child.getParentId());
tree.addChild(parent, child);
}

treeNodeArrayList = tree.searchDepthFirst();
if (!showRoot()) {
treeNodeArrayList.remove(0);
}

return tree;
}


嗯...代码也不长,看一下应该就能懂了。

3. 继承ListView的ExpandableTreeListView

首先我们来看看adapter类是怎么做的呗。

/**
* 服务ExpandableTreeListView的数据源Adapter
* Created by Ivan on 16/2/13.
*/
public abstract class ExpandableTreeAdapter extends BaseAdapter {

private Tree listDataTree;
private ArrayList<TreeNode> treeNodeArrayList;

/**
* 实例化一个ExpandableTreeAdapter,其数据源为基于listData生成的树
* @param listData 数据列表
*/
public ExpandableTreeAdapter(ArrayList<TreeListNodeData> listData) {
listDataTree = generateTree(listData);
}

private Tree generateTree(ArrayList<TreeListNodeData> listData) {
Tree tree = new Tree(listData.get(0));

for (int i=1;i<listData.size();i++) {
TreeNode child = new TreeNode(listData.get(i));
TreeNode parent = tree.findNode(child.getParentId());
tree.addChild(parent, child);
}

treeNodeArrayList = tree.searchDepthFirst();
if (!showRoot()) {
treeNodeArrayList.remove(0);
}

return tree;
}

@Override
public Object getItem(int position) {
return treeNodeArrayList.get(position);
}

@Override
public long getItemId(int position) {
return treeNodeArrayList.get(position).getId();
}

@Override
public int getCount() {
return treeNodeArrayList.size();
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
TreeNode node = treeNodeArrayList.get(position);
TreeListNodeData nodeData = node.getContents();
return getConvertView(nodeData, convertView, parent);
}

@Override
public void notifyDataSetChanged() {
treeNodeArrayList = listDataTree.searchDepthFirst();
if (!showRoot()) {
treeNodeArrayList.remove(0);
}
super.notifyDataSetChanged();
}

/**
* 获取每一个item的view,用法类似于{@link #getView(int, View, ViewGroup)}方法
* @see #getView(int, View, ViewGroup)
* @param data 该item的数据
* @param convertView 重用的view
* @param parent listView引用
* @return 应用data数据的view
*/
protected abstract View getConvertView(TreeListNodeData data, View convertView, ViewGroup parent);

/**
* 是否显示根节点
* @return 是否显示根节点
*/
protected abstract boolean showRoot();
}
嗯...代码也不长,主要就是adapter类可以根据调用者提供的数据构造出一颗树,然后根据深度优先遍历获得列表的显示顺序。内部先实现了BaseAdapter的一些抽象方法,然后又对外提供了两个接口,分别是getConvertView和showRoot方法,其使用看注释应该就明白了。

然后,我们再来看看listView吧。

/**
* 可折叠展开的listView
* Created by Ivan on 16/2/13.
*/
public class ExpandableTreeListView extends ListView {

private ExpandableTreeAdapter expandableTreeAdapter;
private AdapterView.OnItemClickListener onItemClickListener;

public ExpandableTreeListView(Context context) {
super(context);
initView();
}

public ExpandableTreeListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}

private void initView() {
setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(parent, view, position, id);
}
if (expandableTreeAdapter == null) {
return;
}
update(position);
}
});
}

private void update(int position) {
TreeNode node = (TreeNode) expandableTreeAdapter.getItem(position);
if (node.getChildrenNodeList() == null) {
return;
}
if (node.isExpand()) {
node.close();
} else {
node.expand();
}
expandableTreeAdapter.notifyDataSetChanged();
}

public void setOnExpandableListItemClickListener(AdapterView.OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}

@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
if (adapter instanceof ExpandableTreeAdapter) {
this.expandableTreeAdapter = (ExpandableTreeAdapter) adapter;
}
}
}
嗯哈哈哈哈...代码都不长,好处就是不用怎么讲。哈哈哈哈。。。

好吧,还是简单说一下。这里其实主要就是给listView设置一个OnItemClickedListener用来监听用户对每一个item的点击,并根据该item对应树中的节点的展开/收起状态以及是否有孩子节点来对其进行收起/展开。

其实,这个demo分开来讲,每一个模块都没有什么特别难的地方,主要就是树这个东西摸透了就没什么了。

4. finally

嗯...最后嘛,代码写得肯定不是尽善尽美的,毕竟我也只是只小菜鸟,就是出来分享一下而已,各位大神看看就好~~~

最后,奉上github源码~

ExpandableTreeListView github源码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: