1. 程式人生 > >Android 打造隨意層級樹形控件 考驗你的數據結構和設計

Android 打造隨意層級樹形控件 考驗你的數據結構和設計

getparent layout lin throw draw set code 完整 三角形

轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/40212367,本文出自:【張鴻洋的博客】

1、概述

大家在項目中或多或少的可能會見到,偶爾有的項目須要在APP上顯示個樹形控件,比方展示一個機構組織,最上面是boss。然後各種部門。各種小boss,最後各種小羅羅。總體是一個樹形結構。遇到這種情況,大家可能回去百度。由於層次多嘛,可能更easy想到ExpandableListView , 由於這玩意層級比Listview多。可是ExpandableListView實現眼下僅僅支持兩級,當然也有人改造成多級的;可是從我個人角度去看,首先我不喜歡ExpandableListView ,數據集的組織比較復雜。

所以今天帶大家使用ListView來打造一個樹形展示效果。ListView應該是大家再熟悉只是的控件了,而且數據集也就是個List<T> 。

本篇博客目標實現,僅僅要是符合樹形結構的數據能夠輕松的通過我們的代碼,實現樹形效果,有多輕松,文末就知道了~~

好了,既然是要展現樹形結構。那麽數據上肯定就是樹形的一個依賴。也就是說。你的每條記錄,至少有個字段指向它的父節點;相似(id , pId, others ....)

2、原理分析

先看看我們的效果圖:

技術分享

我們支持隨意層級,包括item的布局依舊讓用戶自己的去控制,我們的demo的Item布局非常easy。一個圖標+文本~~

原理就是,樹形不樹形。事實上不就是多個縮進麽,僅僅要能夠推斷每一個item屬於樹的第幾層(術語貌似叫高度),設置合適的縮進就可以。

當然了。原理說起來簡單,還得控制每一層間關系,加入展開縮回等,以及有了縮進還要能顯示在正確的位置,只是沒關系,我會帶著大家一步一步實現的。

3、使用方法

由於總體比較長,我決定首先帶大家看一下使用方法,就是假設學完了這篇博客。我們須要樹形控件,我們須要花多少精力去完畢~~

如今需求來了:我如今須要展示一個文件管理系統的樹形結構:

數據是這種:

//id , pid , label , 其它屬性
		mDatas.add(new FileBean(1, 0, "文件管理系統"));
		mDatas.add(new FileBean(2, 1, "遊戲"));
		mDatas.add(new FileBean(3, 1, "文檔"));
		mDatas.add(new FileBean(4, 1, "程序"));
		mDatas.add(new FileBean(5, 2, "war3"));
		mDatas.add(new FileBean(6, 2, "刀塔傳奇"));

		mDatas.add(new FileBean(7, 4, "面向對象"));
		mDatas.add(new FileBean(8, 4, "非面向對象"));

		mDatas.add(new FileBean(9, 7, "C++"));
		mDatas.add(new FileBean(10, 7, "JAVA"));
		mDatas.add(new FileBean(11, 7, "Javascript"));
		mDatas.add(new FileBean(12, 8, "C"));

當然了,bean能夠有非常多屬性,我們提供你動態的設置樹節點上的顯示、以及不約束id, pid 的命名。你能夠起隨意喪心病狂的屬性名稱;

那麽我們怎樣確定呢?

看下Bean:

package com.zhy.bean;

import com.zhy.tree.bean.TreeNodeId;
import com.zhy.tree.bean.TreeNodeLabel;
import com.zhy.tree.bean.TreeNodePid;

public class FileBean
{
	@TreeNodeId
	private int _id;
	@TreeNodePid
	private int parentId;
	@TreeNodeLabel
	private String name;
	private long length;
	private String desc;

	public FileBean(int _id, int parentId, String name)
	{
		super();
		this._id = _id;
		this.parentId = parentId;
		this.name = name;
	}

}

如今,不用說。應該也知道我們通過註解來確定的。

以下看我們怎樣將這數據轉化為樹

布局文件就一個listview。就補貼了,直接看Activity

package com.zhy.tree_view;

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

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

import com.zhy.bean.FileBean;
import com.zhy.tree.bean.TreeListViewAdapter;

public class MainActivity extends Activity
{
	private List<FileBean> mDatas = new ArrayList<FileBean>();
	private ListView mTree;
	private TreeListViewAdapter mAdapter;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		initDatas();
		mTree = (ListView) findViewById(R.id.id_tree);
		try
		{
			
			mAdapter = new SimpleTreeAdapter<FileBean>(mTree, this, mDatas, 10);
			mTree.setAdapter(mAdapter);
		} catch (IllegalAccessException e)
		{
			e.printStackTrace();
		}

	}

	private void initDatas()
	{

		// id , pid , label , 其它屬性
		mDatas.add(new FileBean(1, 0, "文件管理系統"));
		mDatas.add(new FileBean(2, 1, "遊戲"));
		mDatas.add(new FileBean(3, 1, "文檔"));
		mDatas.add(new FileBean(4, 1, "程序"));
		mDatas.add(new FileBean(5, 2, "war3"));
		mDatas.add(new FileBean(6, 2, "刀塔傳奇"));

		mDatas.add(new FileBean(7, 4, "面向對象"));
		mDatas.add(new FileBean(8, 4, "非面向對象"));

		mDatas.add(new FileBean(9, 7, "C++"));
		mDatas.add(new FileBean(10, 7, "JAVA"));
		mDatas.add(new FileBean(11, 7, "Javascript"));
		mDatas.add(new FileBean(12, 8, "C"));

	}

}

Activity裏面並沒有什麽特殊的代碼。拿到listview。傳入mData,當中初始化了一個Adapter;

看來我們的核心代碼都在我們的Adapter裏面:

那麽看一眼我們的Adapter

package com.zhy.tree_view;

import java.util.List;

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.zhy.tree.bean.Node;
import com.zhy.tree.bean.TreeListViewAdapter;

public class SimpleTreeAdapter<T> extends TreeListViewAdapter<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.list_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);

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

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

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

}

我們的SimpleTreeAdapter繼承了我們的TreeListViewAdapter ; 除此之外,代碼上僅僅須要復寫getConvertView 。 且getConvetView事實上和我們平時的getView寫法一致;

發布出getConvertView 的目的是,讓用戶自己去決定Item的展示效果。其它的代碼,我已經打包成jar了,用的時候導入就可以。

這樣就完畢了我們的樹形控件。

也就是說用我們的樹形控件,僅僅須要將傳統繼承BaseAdapter改為我們的TreeListViewAdapter 。然後去實現getConvertView 就好了。

那麽如今的效果是:

技術分享

默認就全打開了,由於我們也支持動態設置打開的層級。方面使用者使用。

用起來是不是非常隨意,加幾個註解,ListView的Adapater換個類繼承下~~好了。以下開始帶大家一起從無到有的實現~

4、實現


1、思路

我們的思路是這種,我們顯示時。須要非常多屬性,我們須要知道當前節點是否是父節點,當前的層級。他的孩子節點等等。可是用戶的數據集是不固定的,最多僅僅能給出相似id。pId 這種屬性。也就是說,用戶給的bean並不適合我們用於控制顯示,於是我們準備這樣做:

1、在用戶的Bean中提取出必要的幾個元素 id , pId , 以及顯示的文本(通過註解+反射);然後組裝成我們的真正顯示時的Node。即List<Bean> -> List<Node>

2、顯示的並不是是全部的Node。比方某些節點的父節點是關閉狀態,我們須要進行過濾;即List<Node> ->過濾後的List<Node>

3、顯示時,比方點擊父節點,它的子節點會尾隨其後顯示,我們內部是個List,也就是說,這個List的順序也是非常關鍵的;當然排序我們能夠放為步驟一;

最後將過濾後的Node進行顯示,設置左內邊距就可以。

說了這麽多。首先看一眼我們封裝後的Node

2、Node

package com.zhy.tree.bean;

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

import org.w3c.dom.NamedNodeMap;

import android.util.Log;

public class Node
{

	private int id;
	/**
	 * 根節點pId為0
	 */
	private int 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(int id, int 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 int getId()
	{
		return id;
	}

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

	public int getpId()
	{
		return pId;
	}

	public void setpId(int 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); } } } }


包括了樹節點一些常見的屬性,一些常見的方法;對於getLevel,setExpand這些方法。大家能夠好好看看~

有了Node,剛才的使用方法中,出現的就是我們Adapter所繼承的超類:TreeListViewAdapter;核心代碼都在裏面。我們準備去一探到底:

3、TreeListViewAdapter

代碼不是非常長。直接完整的貼出:

package com.zhy.tree.bean;

import java.util.List;

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;

public abstract class TreeListViewAdapter<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
	 * @param context
	 * @param datas
	 * @param defaultExpandLevel
	 *            默認展開幾級樹
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	public TreeListViewAdapter(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); }


首先我們的類繼承自BaseAdapter,然後我們相應的數據集是,過濾出的可見的Node。

我們的構造方法默認接收4個參數:listview,context,mdatas,以及默認展開的級數:0僅僅顯示根節點;

能夠在構造方法中看到:對用戶傳入的數據集做了排序,和過濾的操作。一會再看這些方法,這些方法我們使用了一個TreeHelper進行了封裝。

註:假設你認為你的Item布局十分復雜。且布局會展示Bean的其它數據。那麽為了方便,你能夠讓Node中包括一個泛型T , 每一個Node攜帶與之對於的Bean的全部數據。

能夠看到我們還直接為Item設置了點擊事件。由於我們樹,默認就有點擊父節點展開與關閉;可是為了讓用戶依舊可用點擊監聽,我們自己定義了一個點擊的回調供用戶使用。

當用戶點擊時,默認調用expandOrCollapse方法。將當然節點重置展開標誌,然後又一次過濾出可見的Node。最後notifyDataSetChanged就可以;

其它的方法都是BaseAdapter默認的一些方法了。

以下我們看下TreeHelper中的一些方法:

4、TreeHelper

首先看TreeListViewAdapter構造方法中用到的兩個方法:

/**
	 * 傳入我們的普通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>以及設置Node間關系
		List<Node> nodes = convetData2Node(datas);
		//拿到根節點
		List<Node> rootNodes = getRootNodes(nodes);
		//排序
		for (Node node : rootNodes)
		{
			addNode(result, node, defaultExpandLevel, 1);
		}
		return result;
	}

拿到用戶傳入的數據。轉化為List<Node>以及設置Node間關系,然後根節點,從根往下遍歷進行排序;

接下來看:filterVisibleNode

/**
	 * 過濾出全部可見的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;
	}

過濾Node的代碼非常easy,遍歷全部的Node,僅僅要是根節點或者父節點是展開狀態就加入返回;

最後看看這兩個方法用到的別的一些私有方法:

	/**
	 * 將我們的數據轉化為樹的節點
	 * 
	 * @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)
		{
			int id = -1;
			int pId = -1;
			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 = f.getInt(t);
				}
				if (f.getAnnotation(TreeNodePid.class) != null)
				{
					f.setAccessible(true);
					pId = f.getInt(t);
				}
				if (f.getAnnotation(TreeNodeLabel.class) != null)
				{
					f.setAccessible(true);
					label = (String) f.get(t);
				}
				if (id != -1 && pId != -1 && 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() == n.getId())
				{
					n.getChildren().add(m);
					m.setParent(n);
				} else if (m.getId() == 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.tree_ex);
		} else if (node.getChildren().size() > 0 && !node.isExpand())
		{
			node.setIcon(R.drawable.tree_ec);
		} else
			node.setIcon(-1);

	}

convetData2Node即遍歷用戶傳入的Bean,轉化為Node。當中Id,pId。label通過註解加反射獲取;然後設置Node間關系;

getRootNodes 這個簡單,獲得根節點

addNode :通過遞歸的方式,把一個節點上的全部的子節點等都按順序放入;

setNodeIcon :設置圖標,這裏標明。我們的jar還依賴兩個小圖標。即兩個三角形。假設你認為樹不須要這種圖標,能夠去掉;


5、註解的類

最後就是我們的3個註解類了,沒撒用。就啟到一個標識的作用

TreeNodeId

package com.zhy.tree.bean;

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.zhy.tree.bean;

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.zhy.tree.bean;

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
{

}


5、最後的展望

基於上面的樣例。我們還有非常多地方能夠改善,以下我提一下:

1、Item的布局依賴非常多Bean的屬性。在Node中使用泛型存儲與之相應的Bean。這樣在getConvertView中就能夠通過Node獲取到原本的Bean數據了;

2、關於自己定義或者不要三角圖標;能夠讓TreeListViewAdapter發布出設置圖標的方法,Node全部使用TreeListViewAdapter中設置的圖標;關於不顯示,直接getConverView裏面無論就可以了;

3、我們通過註解得到的Id ,pId , label 。 假設嫌慢,能夠通過回調的方式進行獲取。我們遍歷的時候,去通過Adapter中定義相似:abstract int getId(T t) ;將t作為參數,讓用戶返回id ,相似還有 pid ,label ;這樣循環的代碼須要從ViewHelper提取到Adapter構造方法中;

4、關於設置包括復選框。選擇了多個Node,不要保存position完事。去保存Node中的Id即原Bean的主鍵;然後在getConvertView中對Id進行對照,防止錯亂;

5、關於註解,眼下註解僅僅啟到了標識的左右;事實上還能幹非常多事,比方默認我們任務用戶的id , pid是整形。可是有可能是別的類型;我們能夠通過在註解中設置方法來確定,比如:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeNodeId
{
	Class type() ;
}

@TreeNodeId(type = Integer.class)
	private int _id;

當然了,假設你的需求沒有上述改動的須要,就不須要折騰了~~

到此,我們整個博客就結束了~~設計中假設存在不足,大家能夠自己去改善;希望大家通過本博客學習到的不僅是一個樣例怎樣實現。很多其它的是怎樣設計;當然鄙人能力有限,請大家自行去其糟粕;



源代碼點擊下載(已經打成jar)

源代碼點擊下載(未打成jar版)



博主部分視頻已經上線。假設你不喜歡枯燥的文本。請猛戳(初錄,期待您的支持):

1、高仿微信5.2.1主界面及消息提醒

2、高仿QQ5.0側滑











Android 打造隨意層級樹形控件 考驗你的數據結構和設計