Android 繪制多級(jí)樹形選擇列表實(shí)例代碼
一、概述
前段時(shí)間有個(gè)項(xiàng)目的需要在Android端顯示一個(gè)復(fù)選的多層樹形控件,主要展示一個(gè)公司的組織架構(gòu),類似總部下面有各個(gè)部門,部門之下是組和員工等。另外需要加上展開與回收部門詳情、關(guān)閉部分已開展的布局、勾選等功能。
效果圖如下:
二、思路分析
毫無疑問,對(duì)于這種數(shù)據(jù)可能達(dá)到幾千幾萬行的列表視圖,我們需要選擇recyclerview等具有回收item功能的控件,因此Item的狀態(tài)保持放在Model中而不是View中。
由于原始數(shù)據(jù)是樹形結(jié)構(gòu)的,我們需要先將樹形結(jié)構(gòu)轉(zhuǎn)換為列表數(shù)據(jù),類似根結(jié)點(diǎn) - 父節(jié)點(diǎn)1 - 子結(jié)點(diǎn)1 - 子節(jié)點(diǎn)2 - 父節(jié)點(diǎn)2......這種形式 - 這恰恰是樹的前序遍歷
實(shí)現(xiàn)思路 - 為了更簡(jiǎn)潔明白,左右顛倒處理
三、具體實(shí)現(xiàn)
簡(jiǎn)單的節(jié)點(diǎn)實(shí)現(xiàn)
public abstract class SimpleTreeNode {
//層級(jí)
protected int hierarchy;
//父節(jié)點(diǎn)
protected K parent = null;
//子節(jié)點(diǎn)
protected final List<K> children = new ArrayList<>();
protected boolean isSelected; // 是否被選中
protected boolean isExpand; // 是否展開
}
前序遍歷則發(fā)生在adapter的getItem和getItemCount的時(shí)候
public T getItem(int position) {
int[] cur = {position};
return getNode(topGroups, cur);
}
/**
* 先序遍歷 - 獲取指定位置的節(jié)點(diǎn)
*
* @param nodes nodes
* @param position itemPosition 數(shù)組只是為了實(shí)現(xiàn)手動(dòng)box實(shí)現(xiàn)共享position
* @return MultiSelectNode or null
*/
protected T getNode(List<T> nodes, final int[] position) {
for (T node : nodes) {
if (position[0] == 0) {
return node;
}
position[0]--;
if (node.getChildren().size() > 0) {
T finalNode = getNode(node.getChildren(), position);
if (finalNode != null) {
return finalNode;
}
}
}
return null;
}
/**
* 先序遍歷 - 獲取展示的總長(zhǎng)度 (isExpand = true)
*
* @param nodes nodes
* @return int
*/
protected int getTreeSize(List<T> nodes) {
int size = 0;
for (T node : nodes) {
size++;
size += getTreeSize(node.getChildren());
}
return size;
}
對(duì)于如何實(shí)現(xiàn)展開和收縮的功能,我嘗試了兩種方式:
在渲染item的時(shí)候判斷node.isExpand = false時(shí),對(duì)item進(jìn)行Gone處理,實(shí)際處理發(fā)現(xiàn)列表卡頓非常嚴(yán)重:假設(shè)所有的item都是隱藏的,那么因?yàn)榱斜頉]有顯示全,所有的item都會(huì)進(jìn)行渲染一遍....
數(shù)據(jù)遍歷的時(shí)候?qū)⒎钦归_的數(shù)據(jù)過濾掉:這種方式完美可行,只需要修改下遍歷方法即可
protected int getTreeSize(List<T> nodes) {
int size = 0;
for (T node : nodes) {
size++;
// 展開過濾
if (node.isExpand()) {
size += getTreeSize(node.getChildren());
}
}
return size;
}
protected T getNode(List<T> nodes, final int[] position) {
for (T node : nodes) {
if (position[0] == 0) {
return node;
}
position[0]--;
// 展開過濾
if (node.isExpand() && node.getChildren().size() > 0) {
T finalNode = getNode(node.getChildren(), position);
if (finalNode != null) {
return finalNode;
}
}
}
return null;
}
以上多級(jí)樹形列表的展開與隱藏便完成了,剩下的便是對(duì)樹節(jié)點(diǎn)的一些操作:例如一個(gè)item展開的時(shí)候?qū)ζ渌?jí)item隱藏;一個(gè)item被勾選或取消勾選的時(shí)候改變其父節(jié)點(diǎn)和子節(jié)點(diǎn)的狀態(tài)等。對(duì)于這些操作,我采用了類似Motion Event的方式 - 用事件傳遞與分發(fā)來處理。
比如展開的時(shí)候同級(jí)的item隱藏,其實(shí)便是通知兄弟節(jié)點(diǎn)設(shè)置expand為false。
通知兄弟節(jié)點(diǎn)
勾選的操作稍麻煩,可能需要遞歸通知父節(jié)點(diǎn)檢查更新,以及遞歸通知子節(jié)點(diǎn)勾選操作,取消勾選亦如此。
關(guān)鍵代碼如下
/**
* Class: SimpleTreeNode
* Author: zwgg
* Date: 2017/10/16
* Time: 10:35
* 簡(jiǎn)單的樹節(jié)點(diǎn)模板類
* 這個(gè)自限定泛型可能有點(diǎn)費(fèi)解:用于以基類導(dǎo)出類作為自身的泛型,以實(shí)現(xiàn)模板功能
* 例如:ClassNameA extend SimpleTreeNode< ClassNameA , T >
* @see Enum
*/
public abstract class SimpleTreeNode<K extends SimpleTreeNode<K, T>, T extends TreeNodeEvent> {
//層級(jí)
protected int hierarchy;
//父節(jié)點(diǎn)
protected K parent = null;
//子節(jié)點(diǎn)
protected final List<K> children = new ArrayList<>();
public SimpleTreeNode() {
}
public SimpleTreeNode(int hierarchy) {
this.hierarchy = hierarchy;
}
public void bindingParent(K parent) {
this.parent = parent;
}
public void bindingChild(K child) {
this.children.add(child);
}
public void bindingChildren(List<K> children) {
this.children.clear();
this.children.addAll(children);
}
public void dataBinding(K parent, K child) {
parent.bindingChild(child);
child.bindingParent(parent);
}
public int getHierarchy() {
return hierarchy;
}
public void setHierarchy(int hierarchy) {
this.hierarchy = hierarchy;
}
/**
* 通知父節(jié)點(diǎn)
* @param event event
*/
public void notifyParent(T event) {
if (parent != null) {
event.setNotifyType(TreeNodeEvent.NOTIFY_PARENT);
parent.onEvent(event);
}
}
/**
* 通知子節(jié)點(diǎn)
* @param event event
*/
public void notifyChildren(T event) {
event.setNotifyType(TreeNodeEvent.NOTIFY_CHILDREN);
for (K child : children) {
child.onEvent(event);
}
}
/**
* 通知兄弟節(jié)點(diǎn) - 需要具有相同的Parent
* @param event event
*/
public void notifyBrother(T event) {
if (parent != null) {
event.setNotifyType(TreeNodeEvent.NOTIFY_BROTHER);
for (K child : parent.children) {
if (child != this) {
child.onEvent(event);
}
}
}
}
public abstract void onEvent(T event);
public List<K> getChildren() {
return children;
}
}
業(yè)務(wù)節(jié)點(diǎn)
public class MultiSelectNode<T extends MultiSelectNode<T>> extends SimpleTreeNode<T, MultiSelectEvent> {
private boolean isSelected; // 是否被選中
private boolean isExpand; // 是否展開
/**
* @param hierarchy view層級(jí) - 用于產(chǎn)生viewType
*/
public MultiSelectNode(int hierarchy) {
super(hierarchy);
}
/**
* 事件處理方法
*
* @param event 傳遞得到的事件
*/
@Override
public void onEvent(MultiSelectEvent event) {
switch (event.getNotifyType()) {
case TreeNodeEvent.NOTIFY_CHILDREN:
// 父節(jié)點(diǎn)通知子節(jié)點(diǎn)改變選擇狀態(tài)
if (event.getEventType() == MultiSelectEvent.EVENT_SET_SELECTED) {
// 如果子節(jié)點(diǎn)選擇狀態(tài)有變,則繼續(xù)通知下層節(jié)點(diǎn)改變狀態(tài)
if (event.isSelected() != isSelected()) {
setSelected(event.isSelected());
notifyChildren(event);
}
}
break;
case TreeNodeEvent.NOTIFY_PARENT:
// 子節(jié)點(diǎn)選擇狀態(tài)更改,則通知父節(jié)點(diǎn)重新根據(jù)所有子節(jié)點(diǎn)設(shè)置自身狀態(tài)
if (event.getEventType() == MultiSelectEvent.EVENT_SET_SELECTED) {
if (recheckSelected() != isSelected()) {
setSelected(!isSelected());
// 如果父節(jié)點(diǎn)有變,則繼續(xù)遞歸通知
notifyParent(event);
}
}
break;
case TreeNodeEvent.NOTIFY_BROTHER:
// 通知兄弟節(jié)點(diǎn)改變擴(kuò)展?fàn)顟B(tài)
if (event.getEventType() == MultiSelectEvent.EVENT_SET_EXPAND) {
if (event.isExpand() != isExpand()) {
setExpand(event.isExpand());
}
}
break;
default:
break;
}
}
/**
* 關(guān)閉兄弟節(jié)點(diǎn)擴(kuò)展
*
* @param isExpand 是否擴(kuò)展
*/
public void setOtherGroupsExpand(boolean isExpand) {
MultiSelectEvent event = new MultiSelectEvent(MultiSelectEvent.EVENT_SET_EXPAND);
event.setExpand(isExpand);
notifyBrother(event);
}
/**
* 通知父節(jié)點(diǎn)根據(jù)子節(jié)點(diǎn)設(shè)置狀態(tài)
* 注:選擇具有遞歸性,如果父類狀態(tài)有變會(huì)繼續(xù)通知父類
*/
public void setParentRecheckSelected() {
MultiSelectEvent event = new MultiSelectEvent(MultiSelectEvent.EVENT_SET_SELECTED);
notifyParent(event);
}
/**
* 通知子節(jié)點(diǎn)設(shè)置選擇狀態(tài)
* 注:選擇具有遞歸性,會(huì)設(shè)置所有孩子以及孩子的孩子狀態(tài)
*
* @param isSelected 是否選擇
*/
public void setChildrenSelected(boolean isSelected) {
MultiSelectEvent event = new MultiSelectEvent(MultiSelectEvent.EVENT_SET_SELECTED);
event.setSelected(isSelected);
notifyChildren(event);
}
/**
* 根據(jù)子節(jié)點(diǎn)設(shè)置自身狀態(tài)
*
* @return isSelected boolean
*/
public boolean recheckSelected() {
for (MultiSelectNode child : children) {
if (!child.isSelected()) {
return false;
}
}
return true;
}
public boolean isExpand() {
return isExpand;
}
public void setExpand(boolean expand) {
isExpand = expand;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
}
Event事件
public class TreeNodeEvent {
public static final int NOTIFY_PARENT = 1;
public static final int NOTIFY_CHILDREN = 2;
public static final int NOTIFY_BROTHER = 3;
private int notifyType;
public TreeNodeEvent(){
}
public TreeNodeEvent(int notifyType) {
this.notifyType = notifyType;
}
public int getNotifyType() {
return notifyType;
}
public void setNotifyType(int notifyType) {
this.notifyType = notifyType;
}
}
public class MultiSelectEvent extends TreeNodeEvent {
public static final int EVENT_SET_SELECTED = 1;
public static final int EVENT_SET_EXPAND = 2;
//事件類型
private int eventType;
private boolean isSelected;
private boolean isExpand;
}
詳細(xì)可見Github: https://github.com/zwgg/MultiSelectList
總結(jié)
以上所述是小編給大家介紹的Android 繪制多級(jí)樹形選擇列表實(shí)例代碼,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Android性能調(diào)優(yōu)利器StrictMode應(yīng)用分析
StrictMode意思為嚴(yán)格模式,是用來檢測(cè)程序中違例情況的開發(fā)者工具。最常用的場(chǎng)景就是檢測(cè)主線程中本地磁盤和網(wǎng)絡(luò)讀寫等耗時(shí)的操作。這篇文章給大家介紹Android性能調(diào)優(yōu)利器StrictMode應(yīng)用分析,感興趣的朋友一起看看吧2018-01-01
Android編程實(shí)現(xiàn)隱藏狀態(tài)欄及測(cè)試Activity是否活動(dòng)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)隱藏狀態(tài)欄及測(cè)試Activity是否活動(dòng)的方法,涉及Android界面布局設(shè)置及Activity狀態(tài)操作的相關(guān)技巧,需要的朋友可以參考下2016-10-10
Android中怎樣避免創(chuàng)建不必要的對(duì)象
對(duì)象的創(chuàng)建從來都不是免費(fèi)的. 一個(gè)使用線程分配池的通用垃圾回收器可以讓臨時(shí)對(duì)象的分配變得廉價(jià)一些, 但是分配內(nèi)存總是比不分配要昂貴得多.所以避免創(chuàng)建不必要的對(duì)象是很重要的一方面。2016-08-08
Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動(dòng)控件
這篇文章主要介紹了Android自定義view實(shí)現(xiàn)有header和footer的滾動(dòng)控件,可以在XML中當(dāng)Layout使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-11-11
Android Studio下Flutter環(huán)境搭建圖文教程
這篇文章主要為大家詳細(xì)介紹了Android Studio下Flutter環(huán)境搭建圖文教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
kotlin中的數(shù)據(jù)轉(zhuǎn)換方法(示例詳解)
這篇文章介紹了Kotlin中將數(shù)字轉(zhuǎn)換為字符串和字符串轉(zhuǎn)換為數(shù)字的多種方法,包括使用`toString()`、字符串模板、格式化字符串、處理可空類型等,同時(shí),也詳細(xì)講解了如何安全地進(jìn)行字符串到數(shù)字的轉(zhuǎn)換,并處理了不同進(jìn)制和本地化格式的字符串轉(zhuǎn)換,感興趣的朋友一起看看吧2025-03-03
Android入門之Gallery+ImageSwitcher用法實(shí)例解析
這篇文章主要介紹了Android入門之Gallery+ImageSwitcher用法,對(duì)Android初學(xué)者有很好的參考借鑒價(jià)值,需要的朋友可以參考下2014-08-08
Android中實(shí)現(xiàn)下載和解壓zip文件功能代碼分享
這篇文章主要介紹了Android中實(shí)現(xiàn)下載和解壓zip文件功能代碼分享,本文直接給出了實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-03-03

