Android 打造隨意層級樹形控件 考驗你的數據結構和設計
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 打造隨意層級樹形控件 考驗你的數據結構和設計