android 创建多层树型结构

时间:2022-05-18 13:46:14

在项目开发过程中、遇到一个可能大家都经常会遇到的问题:在一个页面中要显示一个树型结构图,例如组织结构、文件系统等等。一开始是第一层、下面是第二层、第三层....

例如:

android 创建多层树型结构

这样的结构图、想必肯定很常见,最近学习了一种比较好的方法,记录以及分享。

功能分析

一看这个结构,其实就是一个listview,只是在特定情况下显示部分层次的内容以及不显示其它层次内容,既然我们要实现树形结构、那么父层次跟子层次肯定是有联系的,也就是说每一条记录都应该有自己的id,而且必须有一个字段指向父类的Id。这样我们我们就可以理清它们之间的关系了。

功能实现

首先我们要准备一个基础数据bean:

package com.test.tree;


import com.test.tree.utils.TreeNodeId;
import com.test.tree.utils.TreeNodeLabel;
import com.test.tree.utils.TreeNodePid;

/**
* Created by fuweiwei on 2015/12/24.
*/
public class BaseTreeBean {
@TreeNodeId
private String id; //自己id
@TreeNodePid
private String parentId; //上一层id
@TreeNodeLabel
private String name; //名字

public BaseTreeBean(){

}
public BaseTreeBean(String id, String parentId, String name){
this.id = id;
this.parentId = parentId;
this.name = name;

}
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getParentId() {
return parentId;
}

public void setParentId(String parentId) {
this.parentId = parentId;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
这就是我们每一条记录的bean,这里我们使用了注解,后面方便我们通过发射机制来获取值。

然后我们就开始填充数据,创建树形结构:

package com.test.tree;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.widget.ListView;
import android.widget.Toast;

import com.test.tree.utils.Node;
import com.test.tree.utils.TreeListAdapter;

import java.util.ArrayList;
import java.util.List;

/**
* Created by fuweiwei on 2015/12/24.
*/
public class MainActivity extends FragmentActivity {
private List<BaseTreeBean> mDatas = new ArrayList<BaseTreeBean>();
private ListView mLvTree;
private SimpleTreeAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLvTree = (ListView) findViewById(R.id.listview);
initData();

}
public void initData(){
//增加第一层数据
mDatas.add(new BaseTreeBean("1","0","文件夹"));
//增加第二层数据
mDatas.add(new BaseTreeBean("2","1","学习"));
mDatas.add(new BaseTreeBean("3","1","游戏"));
mDatas.add(new BaseTreeBean("4","1","娱乐"));
//增加第三层数据
mDatas.add(new BaseTreeBean("5","2","数学资料"));
mDatas.add(new BaseTreeBean("6","2","语文资料"));
mDatas.add(new BaseTreeBean("7","2","英语资料"));

mDatas.add(new BaseTreeBean("8","3","竞技游戏"));
mDatas.add(new BaseTreeBean("9","3","休闲游戏"));

mDatas.add(new BaseTreeBean("10","4","电影"));
mDatas.add(new BaseTreeBean("11","4","音乐"));
//增加第四层数据
mDatas.add(new BaseTreeBean("12","5","小学数学"));
mDatas.add(new BaseTreeBean("13","5","初中数学"));
mDatas.add(new BaseTreeBean("14","5","高中数学"));

mDatas.add(new BaseTreeBean("15","8","英雄联盟"));
mDatas.add(new BaseTreeBean("16","8","穿越火线"));

mDatas.add(new BaseTreeBean("17","10","大话西游"));
mDatas.add(new BaseTreeBean("18","10","功夫足球"));
//可以一直添加数据层

try {
mAdapter = new SimpleTreeAdapter<BaseTreeBean>(mLvTree, MainActivity.this, mDatas, 0);
mAdapter.setOnTreeNodeClickListener(new TreeListAdapter.OnTreeNodeClickListener() {
@Override
public void onClick(Node node, int position) {
if(node.isLeaf()&&node.getLevel()==3){
Toast.makeText(MainActivity.this,node.getName(),Toast.LENGTH_SHORT).show();
}

}
});
} catch (IllegalAccessException e) {
e.printStackTrace();
}
mLvTree.setAdapter(mAdapter);
}
}

这里的代码不多、首先制造我们需要的树型结构数据,初始化adapter 任何给我们的listview。然后我们看下我们的adapter;

package com.test.tree;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.test.tree.utils.Node;
import com.test.tree.utils.TreeListAdapter;

import java.util.List;


public class SimpleTreeAdapter<T> extends TreeListAdapter<T>
{

public SimpleTreeAdapter(ListView mTree, Context context, List<T> datas,
int defaultExpandLevel) throws IllegalArgumentException,
IllegalAccessException
{
super(mTree, context, datas, defaultExpandLevel);
}

@Override
public View getConvertView(Node node , int position, View convertView, ViewGroup parent)
{

ViewHolder viewHolder = null;
if (convertView == null)
{
convertView = mInflater.inflate(R.layout.adapter_tree_item, parent, false);
viewHolder = new ViewHolder();
viewHolder.icon = (ImageView) convertView
.findViewById(R.id.id_treenode_icon);
viewHolder.label = (TextView) convertView
.findViewById(R.id.id_treenode_label);
convertView.setTag(viewHolder);
viewHolder.next = (ImageView) convertView
.findViewById(R.id.id_treenode_next);

} else
{
viewHolder = (ViewHolder) convertView.getTag();
}

if (node.getIcon() == -1)
{
viewHolder.icon.setVisibility(View.INVISIBLE);
if(node.getLevel()==3){
viewHolder.next.setVisibility(View.VISIBLE);
}else{
viewHolder.next.setVisibility(View.INVISIBLE);
}
} else
{
viewHolder.next.setVisibility(View.INVISIBLE);
viewHolder.icon.setVisibility(View.VISIBLE);
viewHolder.icon.setImageResource(node.getIcon());
}
viewHolder.label.setText(node.getName());

return convertView;
}

private final class ViewHolder
{
ImageView icon;
ImageView next;
TextView label;
}

}

这里跟我们普通继承BaseApter没什么区别,就是给每个Item 赋值,关键的地方来了,TreeListAdapter是什么?,我们来看看:

package com.test.tree.utils;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.ListView;


import java.util.List;

/**
* Created by fuweiwei on 2015/12/24.
*/
public abstract class TreeListAdapter<T> extends BaseAdapter
{

protected Context mContext;
/**
* 存储所有可见的Node
*/
protected List<Node> mNodes;
protected LayoutInflater mInflater;
/**
* 存储所有的Node
*/
protected List<Node> mAllNodes;

/**
* 点击的回调接口
*/
private OnTreeNodeClickListener onTreeNodeClickListener;

public interface OnTreeNodeClickListener
{
void onClick(Node node, int position);
}

public void setOnTreeNodeClickListener(
OnTreeNodeClickListener onTreeNodeClickListener)
{
this.onTreeNodeClickListener = onTreeNodeClickListener;
}

/**
*
* @param mTree listview
* @param context 上下文对象
* @param datas 泛型数据
* @param defaultExpandLevel 默认展开几级树
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public TreeListAdapter(ListView mTree, Context context, List<T> datas,
int defaultExpandLevel) throws IllegalArgumentException,
IllegalAccessException
{
mContext = context;
/**
* 对所有的Node进行排序
*/
mAllNodes = TreeHelper.getSortedNodes(datas, defaultExpandLevel);
/**
* 过滤出可见的Node
*/
mNodes = TreeHelper.filterVisibleNode(mAllNodes);
mInflater = LayoutInflater.from(context);

/**
* 设置节点点击时,可以展开以及关闭;并且将ItemClick事件继续往外公布
*/
mTree.setOnItemClickListener(new OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id)
{
expandOrCollapse(position);

if (onTreeNodeClickListener != null)
{
onTreeNodeClickListener.onClick(mNodes.get(position),
position);
}
}

});

}

/**
* 相应ListView的点击事件 展开或关闭某节点
*
* @param position
*/
public void expandOrCollapse(int position)
{
Node n = mNodes.get(position);

if (n != null)// 排除传入参数错误异常
{
if (!n.isLeaf())
{
n.setExpand(!n.isExpand());
mNodes = TreeHelper.filterVisibleNode(mAllNodes);
notifyDataSetChanged();// 刷新视图
}
}
}

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

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

@Override
public long getItemId(int position)
{
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
Node node = mNodes.get(position);
convertView = getConvertView(node, position, convertView, parent);
// 设置内边距
convertView.setPadding(node.getLevel() * 30, 3, 3, 3);
return convertView;
}

public abstract View getConvertView(Node node, int position,
View convertView, ViewGroup parent);

}
这里的adapter继承了BaseAdapter ,大家可能注意到了这里使用的数据集并不是我们的BaseTreeBean,那是为什么呢,因为用户的数据集是不固定的,只能提供id,pId这样的属性,也就是说,用户给的bean并不适合我们用来控制显示,所有使用了Node,那我们看看Node是什么:

package com.test.tree.utils;

import java.util.ArrayList;
import java.util.List;

/**
* Created by fuweiwei on 2015/12/24.
*/
public class Node
{

private String id;
/**
* 根节点pId为0
*/
private String pId = "0";

private String name;
/**
* 当前的级别
*/
private int level;

/**
* 是否展开
*/
private boolean isExpand = false;

private int icon;

/**
* 下一级的子Node
*/
private List<Node> children = new ArrayList<Node>();

/**
* 父Node
*/
private Node parent;

public Node()
{
}

public Node(String id, String pId, String name)
{
super();
this.id = id;
this.pId = pId;
this.name = name;
}
public int getIcon()
{
return icon;
}

public void setIcon(int icon)
{
this.icon = icon;
}

public String getId()
{
return id;
}

public void setId(String id)
{
this.id = id;
}

public String getpId()
{
return pId;
}

public void setpId(String pId)
{
this.pId = pId;
}

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}

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

public boolean isExpand()
{
return isExpand;
}

public List<Node> getChildren()
{
return children;
}

public void setChildren(List<Node> children)
{
this.children = children;
}

public Node getParent()
{
return parent;
}

public void setParent(Node parent)
{
this.parent = parent;
}

/**
* 是否为跟节点
*
* @return
*/
public boolean isRoot()
{
return parent == null;
}

/**
* 判断父节点是否展开
*
* @return
*/
public boolean isParentExpand()
{
if (parent == null)
return false;
return parent.isExpand();
}

/**
* 是否是叶子界点
*
* @return
*/
public boolean isLeaf()
{
return children.size() == 0;
}

/**
* 获取level
*/
public int getLevel()
{
return parent == null ? 0 : parent.getLevel() + 1;
}

/**
* 设置展开
*
* @param isExpand
*/
public void setExpand(boolean isExpand)
{
this.isExpand = isExpand;
if (!isExpand)
{

for (Node node : children)
{
node.setExpand(isExpand);
}
}
}

}

Node包括树节点的常用属性,包括节点的关闭是否、级别已经子节点、都是用来控制显示的。既然我们数据表bean有了、那怎么跟我们用户的数据集关联起来呢,这里就要使用到一开始我们所说的反射机制了。在TreeListAdapter中有一个TreeHelper、就是用来转化处理我们所需要数据集的。那我们来看下TreeHelper:

package com.test.tree.utils;


import com.test.tree.R;


import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
* Created by fuweiwei on 2015/12/24.
*/
public class TreeHelper
{
/**
* 传入我们的普通bean,转化为我们排序后的Node
*
* @param datas
* @param defaultExpandLevel
* @return
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public static <T> List<Node> getSortedNodes(List<T> datas,
int defaultExpandLevel) throws IllegalArgumentException,
IllegalAccessException

{
List<Node> result = new ArrayList<Node>();
// 将用户数据转化为List<Node>
List<Node> nodes = convetData2Node(datas);
// 拿到根节点
List<Node> rootNodes = getRootNodes(nodes);
// 排序以及设置Node间关系
for (Node node : rootNodes)
{
addNode(result, node, defaultExpandLevel, 1);
}
return result;
}

/**
* 过滤出所有可见的Node
*
* @param nodes
* @return
*/
public static List<Node> filterVisibleNode(List<Node> nodes)
{
List<Node> result = new ArrayList<Node>();

for (Node node : nodes)
{
// 如果为跟节点,或者上层目录为展开状态
if (node.isRoot() || node.isParentExpand())
{
setNodeIcon(node);
result.add(node);
}
}
return result;
}

/**
* 将我们的数据转化为树的节点
*
* @param datas
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
private static <T> List<Node> convetData2Node(List<T> datas)
throws IllegalArgumentException, IllegalAccessException

{
List<Node> nodes = new ArrayList<Node>();
Node node = null;

for (T t : datas)
{
String id = null;
String pId =null;
String label = null;
Class<? extends Object> clazz = t.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields)
{
if (f.getAnnotation(TreeNodeId.class) != null)
{
f.setAccessible(true);
id = (String) f.get(t);
}
if (f.getAnnotation(TreeNodePid.class) != null)
{
f.setAccessible(true);
pId = (String) f.get(t);;
}
if (f.getAnnotation(TreeNodeLabel.class) != null)
{
f.setAccessible(true);
label = (String) f.get(t);
}

if (id!=null && pId != null && label != null)
{
break;
}
}
node = new Node(id, pId, label);
nodes.add(node);
}

/**
* 设置Node间,父子关系;让每两个节点都比较一次,即可设置其中的关系
*/
for (int i = 0; i < nodes.size(); i++)
{
Node n = nodes.get(i);
for (int j = i + 1; j < nodes.size(); j++)
{
Node m = nodes.get(j);
if (m.getpId().equals( n.getId()))
{
n.getChildren().add(m);
m.setParent(n);
} else if (m.getId().equals( n.getpId()))
{
m.getChildren().add(n);
n.setParent(m);
}
}
}

// 设置图片
for (Node n : nodes)
{
setNodeIcon(n);
}
return nodes;
}

private static List<Node> getRootNodes(List<Node> nodes)
{
List<Node> root = new ArrayList<Node>();
for (Node node : nodes)
{
if (node.isRoot())
root.add(node);
}
return root;
}

/**
* 把一个节点上的所有的内容都挂上去
*/
private static void addNode(List<Node> nodes, Node node,
int defaultExpandLeval, int currentLevel)
{

nodes.add(node);
if (defaultExpandLeval >= currentLevel)
{
node.setExpand(true);
}

if (node.isLeaf())
return;
for (int i = 0; i < node.getChildren().size(); i++)
{
addNode(nodes, node.getChildren().get(i), defaultExpandLeval,
currentLevel + 1);
}
}

/**
* 设置节点的图标
*
* @param node
*/
private static void setNodeIcon(Node node)
{
if (node.getChildren().size() > 0 && node.isExpand())
{
node.setIcon(R.drawable.icon_tree_ex);
} else if (node.getChildren().size() > 0 && !node.isExpand())
{
node.setIcon(R.drawable.icon_tree_ec);
} else
node.setIcon(-1);

}

}

方法我们一个个来看:

1、convetData2Node:遍历传进来的bean,通过注解加反射获取id、pid、label转化为Node,然后设置各个Node之间的关系

2、getRootNodes:通过遍历获取根节点

3、addNode:通过递归的方式、把一个节点上的所有的子节点都按顺序放入

4、setNodeIcon:这个比较简单,就是设置每个Item前面的小图标

5、getSortedNodes:这个上面这些方法,把我们传入的数据集转换我们需要的Node数据集

6、filterVisibleNode:通过遍历获取所有可见的Node,只要是根节点获取父节点展开状态就添加返回

上面就是这个Helper的方法,有效的把用户数据转化为我们需要的数据集。

最后看下我们的注解类,作用就是起到标识的作用,用于反射。

TreeNodeId:

package com.test.tree.utils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeNodeId
{
}
TreeNodePid:

package com.test.tree.utils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeNodePid
{
}

TreeNodeLabel:

package com.test.tree.utils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeNodeLabel
{
}

大功告成,以上是我对这个功能的总结,只是用于记录和分享,有什么宝贵的意见欢迎大家指正,下面有Demo源码。


源码下载