Android使用ListView实现一个高性能无限层级显示的树形控件:
最近公司的Android项目里有一个地方需要选择某公司的所有部门,因为手机屏幕有限所以并不能像网页那样显示树状结构,但是如果只是用列表依次显示所有的部门又会让用户很难找到想要找的部门(即使加上搜索功能也很难表现出层级关系),由于系统控件ExpandableListView 只能显示两级,加上数据集的组织比较麻烦,所以就使用ListView来实现如下的树形展示效果。至于为什么使用listview最大的好处就是它自带控件复用功能,我们不用去处理这个复杂的问题。最终效果如下:
分析:
因为要展示的是一个树形结构,所以每一条记录必须拥有一个指向父亲节点的字段。为了体现出层级结构,其实就是增加缩进就可以了。当然说起来很简单,但是做起来的时候会有很多地方值得注意,比如说如何处理展开和收缩,以及跨级展开,收缩,如何得到当前是第几层从而处理缩进等等,再比如说如果数据量比较大的话性能怎么样等等一系列问题。
接下来我们就以层级显示一个公司的所有部门为需求来实现一下,其实只要具有树形结构我们都可以这样做。
实现思路以及使用方法:
首先我们要定义一个抽象类,其中包含必需的字段和方法:
/**
* Created by HQOCSHheqing on 2016/8/2.
*
* @description 节点抽象类(泛型T主要是考虑到ID和parentID有可能是int型也有可能是String型
* 即这里可以传入Integer或者String,具体什么类型由子类指定
,因为这两种类型比较是否相等的方式不同:一个是用 “==”,一个是用 equals() 函数)
*/
public abstract class Node<T> {
private int _level = -1;//当前节点的层级,初始值-1 后面会讲到
private List<Node> _childrenList = new ArrayList<>();//所有的孩子节点
private Node _parent;//父亲节点
private int _icon;//图标资源ID
private boolean isExpand = false;//当前状态是否展开
public abstract T get_id();//得到当前节点ID
public abstract T get_parentId();//得到当前节点的父ID
public abstract String get_label();//要显示的内容
public abstract boolean parent(Node dest);//判断当前节点是否是dest的父亲节点
public abstract boolean child(Node dest);//判断当前节点是否是dest的孩子节点
public int get_level() {
if (_level == -1){//如果是 -1 的话就递归获取
//因为是树形结构,所以此处想要得到当前节点的层级
//,必须递归调用,但是递归效率低下,如果每次都递归获取会严重影响性能,所以我们把第一次
//得到的结果保存起来避免每次递归获取
int level = _parent == null ? 1 : _parent.get_level()+1;
_level = level;
return _level;
}
return _level;
}
public void set_level(int _level) {
this._level = _level;
}
public List<Node> get_childrenList() {
return _childrenList;
}
public void set_childrenList(List<Node> _childrenList) {
this._childrenList = _childrenList;
}
public Node get_parent() {
return _parent;
}
public void set_parent(Node _parent) {
this._parent = _parent;
}
public int get_icon() {
return _icon;
}
public void set_icon(int _icon) {
this._icon = _icon;
}
public boolean isExpand() {
return isExpand;
}
public void setIsExpand(boolean isExpand) {
this.isExpand = isExpand;
if (isExpand){
_icon = R.mipmap.collapse;
}else{
_icon = R.mipmap.expand;
}
}
public boolean isRoot(){
return _parent == null;
}
public boolean isLeaf(){
return _childrenList.size() <= 0;
}
}
将我们要树状显示的实体继承此抽象类,我们一部门ID以及parentid为integer型为例:
/**
* Created by HQOCSHheqing on 2016/8/2.
*
* @description 部门类(继承Node),此处的泛型Integer是因为ID和parentID都为int
* ,如果为String传入泛型String即可,如果传入String,记得修改<span style="font-family: Arial, Helvetica, sans-serif;">parent和child方法,因为比较相等的方式不同。</span>
*/
public class Dept extends Node<Integer>{
private int id;//部门ID
private int parentId;//父亲节点ID
private String name;//部门名称
public Dept() {
}
public Dept(int id, int parentId, String name) {
this.id = id;
this.parentId = parentId;
this.name = name;
}
/**
* 此处返回节点ID
* @return
*/
@Override
public Integer get_id() {
return id;
}
/**
* 此处返回父亲节点ID
* @return
*/
@Override
public Integer get_parentId() {
return parentId;
}
@Override
public String get_label() {
return name;
}
@Override
public boolean parent(Node dest) {
if (id == ((Integer)dest.get_parentId()).intValue()){
return true;
}
return false;
}
@Override
public boolean child(Node dest) {
if (parentId == ((Integer)dest.get_id()).intValue()){
return true;
}
return false;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getParentId() {
return parentId;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
写一个帮助类:主要是整理节点与节点之间的关系,构造森林(有可能构造出多个树)。
/**
* Created by HQOCSHheqing on 2016/8/2.
*
* @description 节点帮助类
*/
public class NodeHelper {
/**
* 传入所有的要展示的节点数据
* @param nodeList 返回值是所有的根节点
* @return
*/
public static List<Node> sortNodes(List<Node> nodeList){
List<Node> rootNodes = new ArrayList<>();
int size = nodeList.size();
Node m;
Node n;
//两个for循环整理出所有数据之间的斧子关系,最后会构造出一个森林(就是可能有多棵树)
for (int i = 0;i < size;i++){
m = nodeList.get(i);
for (int j = i+1;j < size;j++){
n = nodeList.get(j);
if (m.parent(n)){
m.get_childrenList().add(n);
n.set_parent(m);
}else if (m.child(n)){
n.get_childrenList().add(m);
m.set_parent(n);
}
}
}
//找出所有的树根,同事设置相应的图标(后面你会发现其实这里的主要
// 作用是设置叶节点和非叶节点的图标)
for (int i = 0;i < size;i++){
m = nodeList.get(i);
if (m.isRoot()){
rootNodes.add(m);
}
setNodeIcon(m);
}
nodeList.clear();//此时所有的关系已经整理完成了
// ,森林构造完成,可以清空之前的数据,释放内存,提高性能
// ,如果之前的数据还有用的话就不清空
nodeList = rootNodes;//返回所有的根节点
rootNodes = null;
return nodeList;
}
/**
* 设置图标
* @param node
*/
private static void setNodeIcon(Node node){
if (!node.isLeaf()){
if (node.isExpand()){
node.set_icon(R.mipmap.collapse);
}else{
node.set_icon(R.mipmap.expand);
}
}else{
node.set_icon(-1);
}
}
}
为ListView构造出一个适配器:这个适配器就是经常用到的适配器的写法:
/**
* Created by HQOCSHheqing on 2016/8/3.
*
* @description 适配器类,就是listview最常见的适配器写法
*/
public class NodeTreeAdapter extends BaseAdapter{
//大家经常用ArrayList,但是这里为什么要使用LinkedList
// ,后面大家会发现因为这个list会随着用户展开、收缩某一项而频繁的进行增加、删除元素操作,
// 因为ArrayList是数组实现的,频繁的增删性能低下,而LinkedList是链表实现的,对于频繁的增删
//操作性能要比ArrayList好。
private LinkedList<Node> nodeLinkedList;
private LayoutInflater inflater;
private int retract;//缩进值
private Context context;
public NodeTreeAdapter(Context context,ListView listView,LinkedList<Node> linkedList){
inflater = LayoutInflater.from(context);
this.context = context;
nodeLinkedList = linkedList;
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
expandOrCollapse(position);
}
});
//缩进值,大家可以将它配置在资源文件中,从而实现适配
retract = (int)(context.getResources().getDisplayMetrics().density*10+0.5f);
}
/**
* 展开或收缩用户点击的条目
* @param position
*/
private void expandOrCollapse(int position){
Node node = nodeLinkedList.get(position);
if (node != null && !node.isLeaf()){
boolean old = node.isExpand();
if (old){
List<Node> nodeList = node.get_childrenList();
int size = nodeList.size();
Node tmp = null;
for (int i = 0;i < size;i++){
tmp = nodeList.get(i);
if (tmp.isExpand()){
collapse(tmp,position+1);
}
nodeLinkedList.remove(position+1);
}
}else{
nodeLinkedList.addAll(position + 1, node.get_childrenList());
}
node.setIsExpand(!old);
notifyDataSetChanged();
}
}
/**
* 递归收缩用户点击的条目
* 因为此中实现思路是:当用户展开某一条时,就将该条对应的所有子节点加入到nodeLinkedList
* ,同时控制缩进,当用户收缩某一条时,就将该条所对应的子节点全部删除,而当用户跨级缩进时
* ,就需要递归缩进其所有的孩子节点,这样才能保持整个nodeLinkedList的正确性,同时这种实
* 现方式避免了每次对所有数据进行处理然后插入到一个list,最后显示出来,当数据量一大,就会卡顿,
* 所以这种只改变局部数据的方式性能大大提高。
* @param position
*/
private void collapse(Node node,int position){
node.setIsExpand(false);
List<Node> nodes = node.get_childrenList();
int size = nodes.size();
Node tmp = null;
for (int i = 0;i < size;i++){
tmp = nodes.get(i);
if (tmp.isExpand()){
collapse(tmp,position+1);
}
nodeLinkedList.remove(position+1);
}
}
@Override
public int getCount() {
return nodeLinkedList.size();
}
@Override
public Object getItem(int position) {
return nodeLinkedList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
if (convertView == null){
convertView = inflater.inflate(R.layout.tree_listview_item,null);
holder = new ViewHolder();
holder.imageView = (ImageView)convertView.findViewById(R.id.id_treenode_icon);
holder.label = (TextView)convertView.findViewById(R.id.id_treenode_label);
holder.confirm = (LinearLayout)convertView.findViewById(R.id.id_confirm);
convertView.setTag(holder);
}else{
holder = (ViewHolder)convertView.getTag();
}
Node node = nodeLinkedList.get(position);
holder.label.setText(node.get_label());
if(node.get_icon() == -1){
holder.imageView.setVisibility(View.INVISIBLE);
}else{
holder.imageView.setVisibility(View.VISIBLE);
holder.imageView.setImageResource(node.get_icon());
}
holder.confirm.setTag(position);
holder.confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context,"选中:"+v.getTag(),Toast.LENGTH_SHORT).show();
}
});
convertView.setPadding(node.get_level()*retract,5,5,5);//处理缩进
return convertView;
}
static class ViewHolder{
public ImageView imageView;
public TextView label;
public LinearLayout confirm;
}
}
所有重点都在注释中说明,此处省略。
最后我们来应用一下:
首先创建一个activity:
/**
* Created by HQOCSHheqing on 2016/8/4.
*
* @description
*/
public class TreeTestActivity extends Activity{
private ListView mListView;
private NodeTreeAdapter mAdapter;
private LinkedList<Node> mLinkedList = new LinkedList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.select_dept_layout);
mListView = (ListView)findViewById(R.id.id_tree);
mAdapter = new NodeTreeAdapter(this,mListView,mLinkedList);
mListView.setAdapter(mAdapter);
initData();
}
private void initData(){
List<Node> data = new ArrayList<>();
addOne(data);
mLinkedList.addAll(NodeHelper.sortNodes(data));
mAdapter.notifyDataSetChanged();
}
private void addOne(List<Node> data){
data.add(new Dept(1, 0, "总公司"));//可以直接注释掉此项,即可构造一个森林
data.add(new Dept(2, 1, "一级部一级部门一级部门一级部门门级部门一级部门级部门一级部门一级部门门级部一级"));
data.add(new Dept(3, 1, "一级部门"));
data.add(new Dept(4, 1, "一级部门"));
data.add(new Dept(222, 5, "二级部门--测试1"));
data.add(new Dept(223, 5, "二级部门--测试2"));
data.add(new Dept(5, 1, "一级部门"));
data.add(new Dept(224, 5, "二级部门--测试3"));
data.add(new Dept(225, 5, "二级部门--测试4"));
data.add(new Dept(6, 1, "一级部门"));
data.add(new Dept(7, 1, "一级部门"));
data.add(new Dept(8, 1, "一级部门"));
data.add(new Dept(9, 1, "一级部门"));
data.add(new Dept(10, 1, "一级部门"));
for (int i = 2;i <= 10;i++){
for (int j = 0;j < 10;j++){
data.add(new Dept(1+(i - 1)*10+j,i, "二级部门"+j));
}
}
for (int i = 0;i < 5;i++){
data.add(new Dept(101+i,11, "三级部门"+i));
}
for (int i = 0;i < 5;i++){
data.add(new Dept(106+i,22, "三级部门"+i));
}
for (int i = 0;i < 5;i++){
data.add(new Dept(111+i,33, "三级部门"+i));
}
for (int i = 0;i < 5;i++){
data.add(new Dept(115+i,44, "三级部门"+i));
}
for (int i = 0;i < 5;i++){
data.add(new Dept(401+i,101, "四级部门"+i));
}
}
}
布局文件:
#select_dept_layout.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/id_tree"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="#aaa"
android:dividerHeight="1px"/>
</RelativeLayout>
#tree_listview_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/id_treenode_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:src="@mipmap/expand" />
<!--保证整块区域可点,使用户好点击-->
<LinearLayout
android:id="@+id/id_confirm"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:clickable="true"
android:paddingBottom="8dp"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="8dp">
<ImageView
android:layout_width="25dp"
android:layout_height="25dp"
android:background="@drawable/login_checkbox_selector"
android:scaleType="centerInside" />
</LinearLayout>
<TextView
android:id="@+id/id_treenode_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/id_confirm"
android:layout_toRightOf="@id/id_treenode_icon"
android:textColor="@android:color/black"
android:textSize="12sp" />
</RelativeLayout>
实现思路:
要实现一个层级显示的控件,但是又不确定有几层,而内置的ExpandableListView又只能显示两级关系,所以只能自己改造已有的控件或者完全自己写,但是再仔细考虑会发现这两种方式都不太好实现,都需要我们自己处理层级,而且完全自己写还有一个致命问题就是复用问题,即使实现了也会有很多bug,想想会让我们无从下手。但是我们换一种思路,要想展示层级关系,不就是父级与子级之间的缩进不同吗,所以我们是否可以通过控制ListView的各个条目的缩进从而达到这个目的,这样我们就不用管复杂的复用问题了,但是怎么才能实现一层一层的展开、收缩呢?不要想的太复杂,换一种思路,其实就是更改ListView的数据集,展开时将要展开的数据放入到数据集中,收缩时从数据集中删除相应数据即可,这样我们每次只处理与用户点击有关的局部数据,而不用每次遍历所有的数据进行筛选,这样性能会大大提高。
在实现这个功能之前也上网找了资料,其中深受这篇博客(http://blog.csdn.net/lmj623565791/article/details/40212367)的启发,是站在巨人的肩膀上,在此感谢。
完整源代码:[http://download.csdn.net/detail/hqocshheqing/9601185][http_download.csdn.net_detail_hqocshheqing_9601185]
github地址:[https://github.com/heqinghqocsh/TreeView][https_github.com_heqinghqocsh_TreeView]