Android 打造任意層級樹形控制項 考驗你的資料結構和設計,android層級
轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/40212367,本文出自:【張鴻洋的部落格】1、概述
大家在項目中或多或少的可能會見到,偶爾有的項目需要在APP上顯示個樹形控制項,比如展示一個機構組織,最上面是boss,然後各種部門,各種小boss,最後各種小羅羅;整體是一個樹形結構;遇到這樣的情況,大家可能回去百度,因為層次多嘛,可能更容易想到ExpandableListView , 因為這玩意層級比Listview多,但是ExpandableListView實現目前只支援兩級,當然也有人改造成多級的;但是從我個人角度去看,首先我不喜歡ExpandableListView ,資料集的組織比較複雜。所以今天帶大家使用ListView來打造一個樹形展示效果。ListView應該是大家再熟悉不過的控制項了,並且資料集也就是個List<T> 。
本篇部落格目標實現,只要是符合樹形結構的資料可以輕鬆的通過我們的代碼,實現樹形效果,有多輕鬆,文末就知道了~~
好了,既然是要展現樹形結構,那麼資料上肯定就是樹形的一個依賴,也就是說,你的每條記錄,至少有個欄位指向它的父節點;類似(id , pId, others ....)
2、原理分析
先看看我們的:
我們支援任意層級,包括item的布局依然讓使用者自己的去控制,我們的demo的Item布局很簡單,一個表徵圖+文本~~
原理就是,樹形不樹形,其實不就是多個縮排麼,只要能夠判斷每個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{@TreeNodeIdprivate int _id;@TreeNodePidprivate int parentId;@TreeNodeLabelprivate 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;@Overrideprotected 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);}@Overridepublic 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(){@Overridepublic 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();// 重新整理視圖}}}@Overridepublic int getCount(){return mNodes.size();}@Overridepublic Object getItem(int position){return mNodes.get(position);}@Overridepublic long getItemId(int position){return position;}@Overridepublic 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的代碼很簡單,遍曆所有的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);} elsenode.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版)
---------------------------------------------------------------------------------------------------------
我建了一個QQ群,方便大家交流。群號:55032675
----------------------------------------------------------------------------------------------------------
博主部分視頻已經上線,如果你不喜歡枯燥的文本,請猛戳(初錄,期待您的支援):
1、高仿5.2.1主介面及訊息提醒
2、高仿QQ5.0側滑
C#程式怎設計一個三級的樹形資料結構?
可以仿照TreeView的類型,TreeView的Nodes屬性是TreeNodeCollection類,
public class TreeNodeCollection : IList, ICollection, IEnumerable,
所以你仿照這種結構建立你自己的類,同樣實現這三種介面就行了。
教,對於一個樹形的資料結構怎設計?
這是book的題吧,樓主好好整, NodeList擷取節點的子節點列表