树形结构列表实现(RecyclerView实现)
前言
之前习惯用ListView实现列表功能,但是现在很多项目都开始使用RecyclerView,所以本次实现考虑用RecyclerView实现列表功能,顺便复习一下RecyclerView的使用方法。树形结构的实现,难点在于节点选中或展开收起时需要同时考虑父节点和子节点的展示(展开或收起子节点)和选中情况(部分选中、全部选中、未选中三种状态)。先看一下我实现的效果 当节点A勾选时,其子节点会自动全部勾选中,反之亦然。同时,节点A勾选状态变化时,其父节点会遍历它子节点的变化情况并改变自己的勾选状态(全部勾选、部分勾选或未勾选)。另外,我们用箭头标识该节点是否可以点击展开(向下即没有子节点或者不允许展开,向右则表示有子节点并可以展开)。点击矩形图片框实现勾选状态切换,点击其它地方实现展开收起状态切换。
实现
1.构建节点类Node
根据效果图,我们初步计划节点该有的几个属性,id(唯一标识),pid(当前节点的父节点),level(所在层级),expand(当前是否展开的状态),showText(要显示在界面上的文字),choosed(标识选中的状态,前言中提到选中状态有三种哦),初步得到我们的节点类:
public class Node { //选中情况 public final static int CHOOSE_NONE = -1; public final static int CHOOSE_PART = 0; public final static int CHOOSE_ALL = 1; //展开情况 public final static int CHILD_EXPAND_ALL = 1; public final static int CHILD_EXPAND_PART = 0; public final static int CHILD_EXPAND_NONE = -1; private String id; private String pid; private int level; private boolean expand; private int choosed = CHOOSE_NONE; private String showText; private List<Node> childs = new ArrayList<>(); public Node(String id, String pid, int level, String showText) { this.id = id; this.pid = pid; this.level = level; this.showText = showText; }
额,代码中注释比较少,根据命名基本能看出来变量的意思。细心的朋友可能会发现还多了一个List类型的变量,这个就是实现树形结构的关键了,它可以储存子节点信息。我自己根据需要定义了一个构造方法如上,你也可以根据需要重载构造方法,另外,get和set方法在此省略(注意是省略了截图,不是说不需要这些方法)。
2.RecyclerView的Adapter
数据源的模型有了,那就尝试写个列表,构造点数据看看效果呗。不管是ListView还是RecyclerView都需要适配器Adapter,那么RecyclerView的Adapter要怎么实现呢(部分同学可能早就会了,可以直接略过这部分)?我们定义一个RecycleViewAdapter类去继承RecycleViewAdapter,studio会自动提示你需要去重写重载三个方法onCreateViewHolder()、onBindViewHolder()、getItemCount()。onCreateViewHolder()就是让你构造View的样子,onBindViewHolder()就是让你填充item数据,getItemCount()就是让你告诉它你有多少条数据。以上是我自己的理解,错了可以留言告诉我。哦,adapter里肯定要有数据啊,那我们添加一个属性
List<Node> datas;好了,View的样子一般都是定义布局文件了--adapter_list_item.xml,我的是这样的:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:padding="5dp" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/irrow" android:layout_width="20dp" android:layout_height="20dp"/> <ImageView android:id="@+id/image" android:src="@drawable/tree_checkbox_unselected" android:layout_width="20dp" android:layout_height="20dp"/> <TextView android:id="@+id/text" android:layout_weight="1" android:layout_width="0dp" android:layout_height="20dp"/> </LinearLayout>
所以第一个方法我们就这样写了:
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_list_item, null); return new ViewHolder(view); }
第三个方法好写,我们先完成:
@Override public int getItemCount() { return datas == null ? 0 : datas.size(); }
然后就是最重要的onBindViewHolder()方法了,因为数据和处理逻辑基本都在这里面完成。前面忘了必须得先定义一个ViewHolder类(名字随便取啦),和ListView中经常定义ViewHolder差不多,其实我们第一个重载方法就必须用到这个类了(防止数据过多list卡死而复用View的基础):
public class ViewHolder extends RecyclerView.ViewHolder{ public ImageView imageView; public TextView textView; public ImageView irrow; public ViewHolder(View itemView) { super(itemView); imageView = (ImageView)itemView.findViewById(R.id.image); textView = (TextView)itemView.findViewById(R.id.text); irrow = (ImageView)itemView.findViewById(R.id.irrow); } }
接下来我们看看onBindViewHolder()里我们需要处理的逻辑和数据吧。首先根据效果图,我们知道父节点和子节点左边距是有区别的,这个怎么实现呢
ViewHolder viewHolder = (ViewHolder)holder; holder.itemView.setPadding(30 * datas.get(position).getLevel(), 10 , 10, 10);
然后解决选择显示勾选状态,记得我们Node中定义了勾选状态的属性choosed吧,我们根据它的状态决定具体的图片显示,图片是我从其它项目里考过来的,所以命名可能不太容易懂,将就一下
holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT , LinearLayout.LayoutParams.WRAP_CONTENT)); if(datas != null && datas.get(position) != null){ viewHolder.textView.setText(datas.get(position).getShowText()); if(datas.get(position).isExpand()) { if (datas.get(position).getChoosed() == Node.CHOOSE_PART) { viewHolder.imageView.setImageResource(R.drawable.tree_checkbox_selected); } else if(datas.get(position).getChoosed() == Node.CHOOSE_NONE){ viewHolder.imageView.setImageResource(R.drawable.tree_checkbox_unselected); }else{ viewHolder.imageView.setImageResource(R.drawable.cb_patrol_checked); } } }
怎么实现展开或收起的逻辑呢,同样我们Node里定义了对应属性,我们进行判断
//展开或隐藏 if(!datas.get(position).isExpand() && !TextUtils.isEmpty(datas.get(position).getPid())){ //holder.itemView.setVisibility(View.GONE); //直接使用GONE方法,效果和invisible效果相同,即收起后仍会用空白位置,所以采用以下方法 setVisibility(false, holder.itemView); }else{ //holder.itemView.setVisibility(View.VISIBLE); setVisibility(true, holder.itemView); }
这里需要注意的是,最开始我是考虑通过设置View的setVisibility()方法实现的,但是实践下来会有问题,具体如代码中注释,具体原因暂不清楚,然后找了下解决办法,就是我们自定义了一个方法setVisibility(),通过改变View的布局属性来实现我们的理想效果
public void setVisibility(boolean isVisible, View itemView){ RecyclerView.LayoutParams param = (RecyclerView.LayoutParams)itemView.getLayoutParams(); if(param == null){ return; } if (isVisible){ param.height = LinearLayout.LayoutParams.WRAP_CONTENT; param.width = LinearLayout.LayoutParams.MATCH_PARENT; itemView.setVisibility(View.VISIBLE); }else{ itemView.setVisibility(View.GONE); param.height = 0; param.width = 0; } itemView.setLayoutParams(param); }接着,我们实现每个item前面箭头显示的逻辑处理(需要注意根节点的特殊性)
if(!datas.get(position).getChilds().isEmpty() && NodeUtil.getChildExpandStatus(datas.get(position)) != Node.CHILD_EXPAND_ALL){ viewHolder.irrow.setImageResource(R.drawable.household_right); }else{ viewHolder.irrow.setImageResource(R.drawable.household_down); }
RecyclerView的明显特点(对比ListView)之一就是没了条目点击的监听时间,其实就算有我们貌似这里也用不上,先写选中状态图片的监听时间
viewHolder.imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Node node = datas.get(position); if(node.getChoosed() == Node.CHOOSE_NONE || node.getChoosed() == Node.CHOOSE_PART){ node.setChoosed(Node.CHOOSE_ALL); }else{ node.setChoosed(Node.CHOOSE_NONE); } //datas.get(position).setChoosed(!datas.get(position).isChoosed()); NodeUtil.chooseNodes(datas, datas.get(position), datas.get(position).getChoosed()); notifyDataSetChanged(); } });
前面是实现当前条目点击后,状态置换的逻辑。后面才是要实现当前条目点击后,父节点及子节点的处理,可以看到这里我们自定义了一个方法类NodeUtil,这里单独用一个方法类封装,方便以后对Node的其它处理,先说chooseNodes的实现吧
/** * 设置节点选中情况,当前节点选中情况决定了父节点和子节点的勾选情况 * @param nodes * @param node * @param chooseStatus */ public static void chooseNodes(List<Node> nodes, Node node, int chooseStatus){ //设置子节点 for(Node item : nodes){ if(item.getId().equals(node.getId())){ for(Node child : item.getChilds()){ child.setChoosed(chooseStatus); if(child.getChilds().isEmpty()){ continue; } chooseNodes(nodes, child, chooseStatus); } } } //设置父节点 List<Node> parents = new ArrayList<>(); getParents(nodes, node, parents); for(Node parent : parents) { int countChoosed = 0; if (parent == null) { //本例中只有一个root节点 parent = getNodesRoot(nodes).get(0); } for (Node node1 : parent.getChilds()) { countChoosed = countChoosed + node1.getChoosed(); } if (countChoosed == Node.CHOOSE_ALL * parent.getChilds().size()) { parent.setChoosed(Node.CHOOSE_ALL); } else if (countChoosed == Node.CHOOSE_NONE * parent.getChilds().size()) { parent.setChoosed(Node.CHOOSE_NONE); } else { parent.setChoosed(Node.CHOOSE_PART); } } }
关于算法我也不太其实也不算复杂,细心看应该能看懂。主要是有一个递归调用。说明一下,NodeUtil我还定义了其它的方法,都是在开发过程中发现要使用的Node的处理逻辑。所有的方法我先贴出来
/** * 获取nodes根节点(可能有多个根节点) * @param nodes * @return */ public static List<Node> getNodesRoot(List<Node> nodes){ List<Node> roots = new ArrayList<>(); if(nodes == null || nodes.size() == 0){ return roots; } for(Node node : nodes){ if(TextUtils.isEmpty(node.getPid())){ roots.add(node); } } return roots; } /** * 设置nodes中各节点子节点 * @param nodes * @return */ public static List<Node> tidyNodes(List<Node> nodes){ for(int i = 0; i < nodes.size(); i++){ for(int j = i + 1; j < nodes.size(); j++){ //i 是 j 的父节点 if(nodes.get(i).getId().equals(nodes.get(j).getPid())){ nodes.get(i).getChilds().add(nodes.get(j)); } //j 是 i 的父节点 else if(nodes.get(i).getPid().equals(nodes.get(j).getId())){ nodes.get(j).getChilds().add(nodes.get(i)); } } } return nodes; } /** * 设置显示层级 * @param nodes * @param level */ public static void setShowLevel(List<Node> nodes, int level){ for(Node node : nodes){ if(node.getLevel() <= level){ node.setExpand(true); }else{ node.setExpand(false); } } } /** * 寻找叶节点 * @param nodes * @return */ public static List<Node> findLeafs(List<Node> nodes){ List<Node> leafs = new ArrayList<>(); if(nodes == null || nodes.size() == 0){ return leafs; } for(Node node : nodes){ if(node.getChilds().isEmpty()){ leafs.add(node); } } return leafs; } public static void showNodes(List<Node> nodes, Node node, boolean show){ for(Node item : nodes){ if(item.getId().equals(node.getId())){ for(Node child : item.getChilds()){ child.setExpand(show); if(child.getChilds().isEmpty()){ continue; } showNodes(nodes, child, show); } } } } /** * 设置节点的expand属性,收起或展开 * @param nodes * @param node */ public static void showNodes2(List<Node> nodes, Node node){ boolean show = true; for(Node item : nodes){ if(item.getId().equals(node.getId())){ for(Node child : item.getChilds()){ if(child.isExpand()){ show = false; break; } } } } for(Node item : nodes){ if(item.getId().equals(node.getId())){ for(Node child : item.getChilds()){ child.setExpand(show); showNodes2(item.getChilds(), child, false); } } } } public static void showNodes2(List<Node> nodes, Node node,boolean show){ for(Node item : nodes){ for(Node child : item.getChilds()){ child.setExpand(show); showNodes2(item.getChilds(), child, show); } } } /** * 设置节点选中情况,当前节点选中情况决定了父节点和子节点的勾选情况 * @param nodes * @param node * @param chooseStatus */ public static void chooseNodes(List<Node> nodes, Node node, int chooseStatus){ //设置子节点 for(Node item : nodes){ if(item.getId().equals(node.getId())){ for(Node child : item.getChilds()){ child.setChoosed(chooseStatus); if(child.getChilds().isEmpty()){ continue; } chooseNodes(nodes, child, chooseStatus); } } } //设置父节点 List<Node> parents = new ArrayList<>(); getParents(nodes, node, parents); for(Node parent : parents) { int countChoosed = 0; if (parent == null) { //本例中只有一个root节点 parent = getNodesRoot(nodes).get(0); } for (Node node1 : parent.getChilds()) { countChoosed = countChoosed + node1.getChoosed(); } if (countChoosed == Node.CHOOSE_ALL * parent.getChilds().size()) { parent.setChoosed(Node.CHOOSE_ALL); } else if (countChoosed == Node.CHOOSE_NONE * parent.getChilds().size()) { parent.setChoosed(Node.CHOOSE_NONE); } else { parent.setChoosed(Node.CHOOSE_PART); } } } /** * 通过id查询node * @param nodes * @param id * @return */ public static Node getNode(List<Node> nodes, String id){ for(Node node : nodes){ if(node.getId().equals(id)){ return node; } } return null; } /** * 获取节点的所有父节点(包括间接父节点) * @param nodes * @param node * @param results */ public static void getParents(List<Node> nodes, Node node, List<Node> results){ for(Node item : nodes){ if(TextUtils.isEmpty(item.getPid())){ results.add(item); } else if(node.getPid().equals(item.getId())){ results.add(item); getParents(nodes, item, results); } } } /** * 获取直接子节点的展开状况 * @param node * @return */ public static int getChildExpandStatus(Node node){ if(node.getChilds() == null || node.getChilds().size() == 0){ return Node.CHILD_EXPAND_ALL; }else{ int expandCount = 0; for(Node child : node.getChilds()){ if(child.isExpand()){ expandCount++; } } if(expandCount == node.getChilds().size()){ return Node.CHILD_EXPAND_ALL; }else if(expandCount == 0){ return Node.CHILD_EXPAND_NONE; }else{ return Node.CHILD_EXPAND_PART; } } }
然后文本点击实现展开收起效果
viewHolder.textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //boolean status = datas.get(position).isExpand(); NodeUtil.showNodes2(datas, datas.get(position)); notifyDataSetChanged(); } });
到此我们的Adapter就写完了。最后完成Activity,布局简单(只有一个RecyclerView)我就不贴了,代码部分如下(我们本地构造数据)
private RecyclerView mRecyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findView(); initData(); } private void findView(){ mRecyclerView = (RecyclerView) findViewById(R.id.recycleView); mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); } private void initData(){ Node node1 = new Node("1", "", 0, "动物"); Node node2 = new Node("2", "1", 1, "无脊椎动物"); Node node3 = new Node("3", "2", 2, "原生动物"); Node node4 = new Node("4", "2", 2, "环节动物"); Node node5 = new Node("5", "4", 3, "蚯蚓"); Node node6 = new Node("6", "2", 2, "节肢动物"); Node node7 = new Node("7", "6", 3, "昆虫"); Node node8 = new Node("8", "6", 3, "甲壳动物"); Node node9 = new Node("9", "1", 1, "有脊椎动物"); List<Node> nodes = new ArrayList<>(); nodes.add(node1); nodes.add(node2); nodes.add(node3); nodes.add(node4); nodes.add(node5); nodes.add(node6); nodes.add(node7); nodes.add(node8); nodes.add(node9); RecycleViewAdapter adapter = new RecycleViewAdapter(); adapter.setDatas(nodes); NodeUtil.tidyNodes(nodes); NodeUtil.setShowLevel(nodes, 2); mRecyclerView.setAdapter(adapter); }最后,完美运行,收工。欢迎留言问题讨论。