一、概述
前段时间有个项目的需要在Android端显示一个复选的多层树形控件,主要展示一个公司的组织架构,类似总部下面有各个部门,部门之下是组和员工等。另外需要加上展开与回收部门详情、关闭部分已开展的布局、勾选等功能。
效果图如下:
效果展示二、思路分析
毫无疑问,对于这种数据可能达到几千几万行的列表视图,我们需要选择recyclerview等具有回收item功能的控件,因此Item的状态保持放在Model中而不是View中。
由于原始数据是树形结构的,我们需要先将树形结构转换为列表数据,类似根结点 - 父节点1 - 子结点1 - 子节点2 - 父节点2......这种形式 - 这恰恰是树的前序遍历
实现思路 - 为了更简洁明白,左右颠倒处理三、具体实现
简单的节点实现
public abstract class SimpleTreeNode {
//层级
protected int hierarchy;
//父节点
protected K parent = null;
//子节点
protected final List<K> children = new ArrayList<>();
protected boolean isSelected; // 是否被选中
protected boolean isExpand; // 是否展开
}
前序遍历则发生在adapter的getItem和getItemCount的时候
public T getItem(int position) {
int[] cur = {position};
return getNode(topGroups, cur);
}
/**
* 先序遍历 - 获取指定位置的节点
*
* @param nodes nodes
* @param position itemPosition 数组只是为了实现手动box实现共享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;
}
/**
* 先序遍历 - 获取展示的总长度 (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;
}
对于如何实现展开和收缩的功能,我尝试了两种方式:
-
在渲染item的时候判断node.isExpand = false时,对item进行Gone处理,实际处理发现列表卡顿非常严重:假设所有的item都是隐藏的,那么因为列表没有显示全,所有的item都会进行渲染一遍....
-
数据遍历的时候将非展开的数据过滤掉:这种方式完美可行,只需要修改下遍历方法即可
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; }
以上多级树形列表的展开与隐藏便完成了,剩下的便是对树节点的一些操作:例如一个item展开的时候对其他同级item隐藏;一个item被勾选或取消勾选的时候改变其父节点和子节点的状态等。对于这些操作,我采用了类似Motion Event的方式 - 用事件传递与分发来处理。
比如展开的时候同级的item隐藏,其实便是通知兄弟节点设置expand为false。
通知兄弟节点勾选的操作稍麻烦,可能需要递归通知父节点检查更新,以及递归通知子节点勾选操作,取消勾选亦如此。
关键代码如下
/**
* Class: SimpleTreeNode
* Author: zwgg
* Date: 2017/10/16
* Time: 10:35
* 简单的树节点模板类
* 这个自限定泛型可能有点费解:用于以基类导出类作为自身的泛型,以实现模板功能
* 例如:ClassNameA extend SimpleTreeNode< ClassNameA , T >
* @see Enum
*/
public abstract class SimpleTreeNode<K extends SimpleTreeNode<K, T>, T extends TreeNodeEvent> {
//层级
protected int hierarchy;
//父节点
protected K parent = null;
//子节点
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;
}
/**
* 通知父节点
* @param event event
*/
public void notifyParent(T event) {
if (parent != null) {
event.setNotifyType(TreeNodeEvent.NOTIFY_PARENT);
parent.onEvent(event);
}
}
/**
* 通知子节点
* @param event event
*/
public void notifyChildren(T event) {
event.setNotifyType(TreeNodeEvent.NOTIFY_CHILDREN);
for (K child : children) {
child.onEvent(event);
}
}
/**
* 通知兄弟节点 - 需要具有相同的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;
}
}
业务节点
public class MultiSelectNode<T extends MultiSelectNode<T>> extends SimpleTreeNode<T, MultiSelectEvent> {
private boolean isSelected; // 是否被选中
private boolean isExpand; // 是否展开
/**
* @param hierarchy view层级 - 用于产生viewType
*/
public MultiSelectNode(int hierarchy) {
super(hierarchy);
}
/**
* 事件处理方法
*
* @param event 传递得到的事件
*/
@Override
public void onEvent(MultiSelectEvent event) {
switch (event.getNotifyType()) {
case TreeNodeEvent.NOTIFY_CHILDREN:
// 父节点通知子节点改变选择状态
if (event.getEventType() == MultiSelectEvent.EVENT_SET_SELECTED) {
// 如果子节点选择状态有变,则继续通知下层节点改变状态
if (event.isSelected() != isSelected()) {
setSelected(event.isSelected());
notifyChildren(event);
}
}
break;
case TreeNodeEvent.NOTIFY_PARENT:
// 子节点选择状态更改,则通知父节点重新根据所有子节点设置自身状态
if (event.getEventType() == MultiSelectEvent.EVENT_SET_SELECTED) {
if (recheckSelected() != isSelected()) {
setSelected(!isSelected());
// 如果父节点有变,则继续递归通知
notifyParent(event);
}
}
break;
case TreeNodeEvent.NOTIFY_BROTHER:
// 通知兄弟节点改变扩展状态
if (event.getEventType() == MultiSelectEvent.EVENT_SET_EXPAND) {
if (event.isExpand() != isExpand()) {
setExpand(event.isExpand());
}
}
break;
default:
break;
}
}
/**
* 关闭兄弟节点扩展
*
* @param isExpand 是否扩展
*/
public void setOtherGroupsExpand(boolean isExpand) {
MultiSelectEvent event = new MultiSelectEvent(MultiSelectEvent.EVENT_SET_EXPAND);
event.setExpand(isExpand);
notifyBrother(event);
}
/**
* 通知父节点根据子节点设置状态
* 注:选择具有递归性,如果父类状态有变会继续通知父类
*/
public void setParentRecheckSelected() {
MultiSelectEvent event = new MultiSelectEvent(MultiSelectEvent.EVENT_SET_SELECTED);
notifyParent(event);
}
/**
* 通知子节点设置选择状态
* 注:选择具有递归性,会设置所有孩子以及孩子的孩子状态
*
* @param isSelected 是否选择
*/
public void setChildrenSelected(boolean isSelected) {
MultiSelectEvent event = new MultiSelectEvent(MultiSelectEvent.EVENT_SET_SELECTED);
event.setSelected(isSelected);
notifyChildren(event);
}
/**
* 根据子节点设置自身状态
*
* @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;
}
详细可见Github: https://github.com/zwgg/MultiSelectList
网友评论