演示
不知道为什么放不了gif!_! 先放张图片
分析
之前看见github上由一个类似的库,但是没找的使用入口,自己就写了一个,存在较多问题。
数据
TreeView的数据应该类似树形结构,与android控件解耦,独立于控件存在,所以NodeSource接口表示树形节点数据。
public interface NodeSource {
public String getName();
}
public static class SampleData implements NodeSource {
public String str;
@Override
public String getName() {
return str;
}
@NotNull
@Override
public String toString() {
return "SampleData{" +
"str='" + str + '\'' +
'}';
}
}
public class TreeNode<T extends NodeSource> {
public static enum Type {
NODE,
LEAF
}
private static final int ROOT = 0;
private static final int NODE_DEFAULT = -1;
private Type type;
// 先序遍历Id,标识该节点在整个树中的位置
private int id;
private int parentId;
private String name;
private int level;
private T value;
private List<TreeNode<T>> children;
}
TreeNode类只表示数据,关心数据之间的逻辑关系,不关心父子节点之间的数据交换,TreeNode的构造方法私有,提供静态创建方法入口。
public static TreeNode<? extends NodeSource> createRoot(NodeSource data) {
return new TreeNode<>(TreeNode.Type.NODE, ROOT, ROOT, ROOT, "root", data, null);
}
public static TreeNode<? extends NodeSource> createNode(NodeSource data) {
return new TreeNode<>(TreeNode.Type.NODE, NODE_DEFAULT, NODE_DEFAULT, NODE_DEFAULT, data.getName(), data, null);
}
public static TreeNode<? extends NodeSource> createLeaf(NodeSource data) {
return new TreeNode<>(TreeNode.Type.LEAF, NODE_DEFAULT, NODE_DEFAULT, NODE_DEFAULT, data.getName(), data, null);
}
节点添加孩子时,该叶子节点自动变更类型;获取该节点大小默认加上子节点的个数。数据节点不关心逻辑Id生成,只关心结构
public TreeNode<T> addChild(TreeNode<T> node) {
if (children == null) {
children = new ArrayList<>();
}
this.type = Type.NODE;
node.parentId = id;
node.level = level + 1;
children.add(node);
return this;
}
public TreeNode<T> getChild(int index) {
return (TreeNode<T>) children.get(index);
}
public int getSize() {
return children == null ? 1 : 1 + children.size();
}
数据节点视图
树节点树和特有节点关系的合集,用于展示特定的节点关系逻辑。
public static class TreeNodeView<T extends NodeSource> {
int viewType;
// 当前View
View view;
boolean expanded;
// 当前节点数据
public TreeNode<T> node;
}
例如,数据节点是没展开显示的逻辑关系,但在控件上显示时存在展开关闭逻辑,又和展示视图没有关联,只是一种状态,与数据无关。
适配器
Android RecyclerView的数据适配器,数据来自于数据节点视图。
public abstract class TreeNodeAdapter<T extends NodeSource> extends RecyclerView.Adapter<TreeNodeAdapter.ViewHolder>{
protected Context mContext;
protected TreeNodeView<T> root;
// RecycleView Id和item视图的映射
protected Map<Integer, TreeNodeView<T>> map;
}
适配器用于管理整颗数据节点视图树,包括逻辑Id的生成规则,默认使用先序遍历Id。
private void updateByPre(TreeNode<T> root) {
if (root == null) {
Log.d("tree node", "root is null");
return;
}
Stack<TreeNode<T>> stack = new Stack<>();
TreeNode<T> curr;
stack.push(root);
int curId = 0;
while (!stack.isEmpty()) {
curr = stack.pop();
curr.setId(curId);
updateNodeView(curId, curr);
for (int i = curr.getSize() - 2; i > -1; i--) {
stack.push(curr.getChild(i));
curr.getChild(i).setParentId(curId);
}
curId++;
}
}
private void updateNodeView(int id, TreeNode<T> node) {
// TreeNodeView<T> currView = new TreeNodeView<>();
TreeNodeView<T> currView = map.get(id);
if (currView == null) {
currView = getNodeView();
}
currView.node = node;
map.put(id, currView);
}
updateNodeView用于更新数据节点视图状态。 抽象适配器封装了Adapter的绑定逻辑和ViewHolder,简化数据绑定。ViewHolder抽取一个可见性方法,用于隐藏折叠数据,
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return newViewHolder(parent, getLayoutId(), viewType);
}
@Override
public int getItemViewType(int position) {
return map.get(position).node.getLevel();
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TreeNodeView<T> nodeView = map.get(position);
TreeNodeView<T> parentView = map.get(nodeView.node.getParentId());
if (nodeView == parentView || (parentView != null && parentView.expanded)) {
holder.setVisibility(true);
bindNode(holder, nodeView, position);
} else {
// 折叠父节点同时折叠子节点
nodeView.expanded = false;
holder.setVisibility(false);
}
map.get(nodeView.node.getId()).view = holder.itemView;
}
protected abstract int getLayoutId();
protected abstract TreeNodeView<T> getNodeView();
protected abstract ViewHolder newViewHolder(ViewGroup parent, int layoutId, int viewType);
protected abstract void bindNode(ViewHolder viewHolder, TreeNodeView<T> node, int position);
protected abstract static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
public void setVisibility(boolean flag) {
RecyclerView.LayoutParams param = (RecyclerView.LayoutParams) itemView.getLayoutParams();
if (flag) {
param.height = RecyclerView.LayoutParams.WRAP_CONTENT;
param.width = RecyclerView.LayoutParams.MATCH_PARENT;
} else {
param.height = 0;
param.width = 0;
}
itemView.setLayoutParams(param);
}
}
样例适配器
节点内容
public static class SampleData implements NodeSource {
public String str;
@Override
public String getName() {
return str;
}
@NotNull
@Override
public String toString() {
return "SampleData{" +
"str='" + str + '\'' +
'}';
}
}
样例数据节点视图
public static class SampleNodeView extends TreeNodeView<SampleData> {
public boolean isAllCheck;
}
样例适配器
@Override
protected int getLayoutId() {
return R.layout.item_tree_view_sample;
}
@Override
protected SampleNodeView getNodeView() {
return new SampleNodeView();
}
@Override
protected TreeNodeAdapter.ViewHolder newViewHolder(ViewGroup parent, int layoutId, int viewType) {
return new ViewHolder(LayoutInflater.from(mContext).inflate(layoutId, parent, false));
}
@Override
protected void bindNode(TreeNodeAdapter.ViewHolder viewHolder, TreeNodeView<SampleData> nodeView, int position) {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) viewHolder.itemView.getLayoutParams();
layoutParams.topMargin = 0;
layoutParams.bottomMargin = 0;
layoutParams.leftMargin = 20 * nodeView.node.getLevel();
ViewHolder vh = (ViewHolder) viewHolder;
SampleNodeView node = (SampleNodeView) nodeView;
vh.mCb.setTag(position);
vh.mTvName.setText(((SampleData) nodeView.node.getValue()).str);
vh.mIvIcon.setVisibility(nodeView.node.getType() == TreeNode.Type.NODE ? View.VISIBLE : View.INVISIBLE);
vh.mIvIcon.setRotation(nodeView.expanded ? 90 : 0);
vh.mCb.setChecked(node.isAllCheck);
// 使用onCheckedListener会触发复用问题
((ViewHolder) viewHolder).mCb.setOnClickListener(v -> {
// 叶子节点
SampleNodeView view = (SampleNodeView) nodeView;
view.isAllCheck = !view.isAllCheck;
vh.mCb.setChecked(view.isAllCheck);
// 自己是父节点,则子节点全部选中
setChildCheck(view.node.getId(), view.isAllCheck);
// 获取父节点,判断父节点是否需要选中,只有在子节点全选的情况下,父节点才选中
SampleNodeView parent = (SampleNodeView) map.get(nodeView.node.getParentId());
// 遍历该子节点的父节点所有子节点的选中状态
setParentCheck(node.node.getParentId(), view.isAllCheck);
new Handler().post(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
});
((ViewHolder) viewHolder).mCb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
}
});
viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (nodeView.node.getType() == TreeNode.Type.NODE) {
if (!nodeView.expanded) {
iconAnim = ObjectAnimator.ofFloat(vh.mIvIcon, "rotation", 0.0f, 90.0f);
} else {
iconAnim = ObjectAnimator.ofFloat(vh.mIvIcon, "rotation", 90.0f, 0.0f);
}
// 折叠
nodeView.expanded = !nodeView.expanded;
startAnim(vh, nodeView, position);
}
}
});
}
private void setParentCheck(int parentId, boolean isAllCheck) {
SampleNodeView parent = (SampleNodeView) map.get(parentId);
if(isAllCheck) {
if (!parent.isAllCheck) {
boolean isParent = true;
for (int i = 0; i < parent.node.getSize() - 1; i++) {
TreeNode<SampleData> child = parent.node.getChild(i);
SampleNodeView childView = (SampleNodeView) map.get(child.getId());
if (!childView.isAllCheck) {
isParent = false;
break;
}
}
parent.isAllCheck = isParent;
}
} else {
parent.isAllCheck = false;
if (parentId != 0) {
setParentCheck(parent.node.getParentId(), parent.isAllCheck);
}
}
}
private void setChildCheck(int id, boolean isChecked) {
SampleNodeView nodeView = (SampleNodeView) map.get(id);
if (nodeView.node.getType() == TreeNode.Type.NODE) {
for (int i = 0; i < nodeView.node.getSize() - 1; i++) {
SampleNodeView child = (SampleNodeView) map.get(nodeView.node.getChild(i).getId());
child.isAllCheck = isChecked;
setChildCheck(child.node.getId(), child.isAllCheck);
}
}
}
static class ViewHolder extends TreeNodeAdapter.ViewHolder {
public ImageView mIvIcon;
public TextView mTvName;
public CheckBox mCb;
public ViewHolder(@NonNull View itemView) {
super(itemView);
mIvIcon = itemView.findViewById(R.id.iv_item_icon);
mTvName = itemView.findViewById(R.id.tv_item_tree_name);
mCb = itemView.findViewById(R.id.cb_item_tree);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_item_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_right"/>
<TextView
android:id="@+id/tv_item_tree_name"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="20sp"
android:text="2333"/>
<CheckBox
android:id="@+id/cb_item_tree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
样例视图中存在复选框,使用RecyclerView会触发复用控件OnChecked事件,因此监听OnClick事件,父子节点间的选中状态存在关联,需要遍历节点树完成对视图的更新。
使用
private void addLevel1() {
SampleAdapter.SampleData root = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> rootNode = (TreeNode<SampleAdapter.SampleData>) TreeNode.createRoot(root);
root.str = "root";
for (int i = 0; i < 10; i++) {
SampleAdapter.SampleData data = new SampleAdapter.SampleData();
data.str = "level-"+rootNode.getLevel()+":"+i;
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createLeaf(data);
rootNode.addChild(node);
}
sampleAdapter = new SampleAdapter(this);
sampleAdapter.setValue(root);
sampleAdapter.setRoot(rootNode);
}
private void addLevel2() {
TreeNode<SampleAdapter.SampleData> child1 = sampleAdapter.getRoot().node.getChild(1);
child1.setType(TreeNode.Type.NODE);
for (int i = 0; i < 2; i++) {
SampleAdapter.SampleData child = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createLeaf(child);
child.str = child1.getName() +"-"+ i;
child1.addChild(node);
}
TreeNode<SampleAdapter.SampleData> child3 = sampleAdapter.getRoot().node.getChild(3);
child3.setType(TreeNode.Type.NODE);
for (int i = 0; i < 4; i++) {
SampleAdapter.SampleData child = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createLeaf(child);
child.str = child3.getName() +"-"+ i;
child3.addChild(node);
}
TreeNode<SampleAdapter.SampleData> child4 = sampleAdapter.getRoot().node.getChild(4);
child4.setType(TreeNode.Type.NODE);
for (int i = 0; i < 5; i++) {
SampleAdapter.SampleData child = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createNode(child);
child.str = child4.getName() +"-"+ i;
child4.addChild(node);
}
TreeNode<SampleAdapter.SampleData> child7 = sampleAdapter.getRoot().node.getChild(7);
child7.setType(TreeNode.Type.NODE);
for (int i = 0; i < 3; i++) {
SampleAdapter.SampleData child = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createLeaf(child);
child.str = child7.getName() +"-"+ i;
child7.addChild(node);
}
}
private void addLevel3() {
TreeNode<SampleAdapter.SampleData> child = sampleAdapter.getRoot().node.getChild(3).getChild(2);
child.setType(TreeNode.Type.NODE).setName("hello");
for (int i = 0; i < 6; i++) {
SampleAdapter.SampleData data = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createLeaf(data);
data.str = child.getName() +"@"+ i;
child.addChild(node);
}
}
private void addLevel4() {
TreeNode<SampleAdapter.SampleData> child = sampleAdapter.getRoot().node.getChild(3).getChild(2).getChild(4);
child.setType(TreeNode.Type.NODE).setName("world");
for (int i = 0; i < 1; i++) {
SampleAdapter.SampleData data = new SampleAdapter.SampleData();
TreeNode<SampleAdapter.SampleData> node = (TreeNode<SampleAdapter.SampleData>) TreeNode.createLeaf(data);
data.str = child.getName() +"#"+ i;
child.addChild(node);
}
}
不足
继承的RecyclerView和Adapter存在回收复用问题,在每次折叠和展开之后会重新创建大量的ViewHolder,无法利用。Gif无法上传,站长有空解决一下^_^!水平有限,望各位大佬指点。