Android 多层级列表展示

  • Post author:
  • Post category:其他


最近工作中需要一个产品选择的多层级列表展示的效果,之后会有图展示。从一开始毫无头绪到能够把他做出来,确实让技术不高的自己费了一把。现在就来记录下,和大家探讨下。

本篇是多层级列表展示的又一个轮子, 是在鸿洋大神的基础之上又添加的逻辑。采用 ListView 做展示(也可以选择RecyclerView做尝试),DataBanding 替换 findViewById 以及 Adapter 中ViewHodler 的使用。

接下来讲分几条做展示:

  • 如何使用
  • xml 样式
  • 数据类型处理
  • 工具类 TreeHelper
  • adapter编写

1. 如何使用

首先我们看下主Activity中如何展示

public class MainActivity extends AppCompatActivity {

    public static final int DEFAULT_EXPAND = 1;
    private ListView mListView;
    private ExpandTreeListAdapter mAdapter;
    private ActivityMainBinding mBinding;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        initData();
    }

    public void initData() {
        List<FileBean> list = new ArrayList<>();
        list.add(new FileBean("1", "0", "姓氏", "1"));
        list.add(new FileBean("01", "1", "周", "2"));
        list.add(new FileBean("02", "1", "王", "2"));
        list.add(new FileBean("03", "1", "理", "2"));
        list.add(new FileBean("2", "0", "名字", "1"));
        list.add(new FileBean("04", "2", "分类", "2"));
        list.add(new FileBean("001", "04", "hahahah", "3"));
        list.add(new FileBean("003", "04", "yayayyayya", "3"));
        list.add(new FileBean("3", "0", "事迹", "1"));
        list.add(new FileBean("07", "3", "你猜", "2"));
        list.add(new FileBean("08", "3", "你再猜", "2"));
        list.add(new FileBean("09", "3", "你猜到了吗", "2"));
        list.add(new FileBean("10", "3", "haha", "2"));
        mAdapter = new ExpandTreeListAdapter<FileBean>(this, mBinding.lvMainList, list, R.layout.item_list_activity_layout, BR.node, DEFAULT_EXPAND);
        mBinding.setAdapter(mAdapter);
    }
}
复制代码

2.xml 样式

我们根据 DataBinding 进行数据绑定,把 adapter 和 listView 绑定,UI 和 数据绑定。

main_xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="view"
            type="com.xiaohong.treelist.MainActivity" />

        <variable
            name="adapter"
            type="com.xiaohong.treelist.adapter.ExpandTreeListAdapter" />
    </data>

    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.xiaohong.treelist.MainActivity">

        <ListView
            android:id="@+id/lv_main_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:adapter="@{adapter}" />

    </android.support.constraint.ConstraintLayout>
</layout>
复制代码

item的布局

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="node"
            type="com.xiaohong.treelist.bean.Node"/>
    </data>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_item_list_text"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@{node.name}"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/iv_item_list_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </LinearLayout>
</layout>
复制代码

3.数据类型处理

多层级的展示,数据形式就是树形结构,每个节点的类型(Node)我们大致可以统一,一般包括名字,id,层级level,孩子节点,其他展示的标志可以按需增加。

如果我们从服务器或者asserts 文件中拿到的数据源的类型可能和我们想要展示的数据的类型不一样,我们可以使用注解和反射转化为我们想要的Node 类型。

FileBean, 服务器数据类型
public class FileBean  {
    @TreeNodeId
    public String code;
    @TreeNodePId
    public String parentCode;
    @TreeNodeLable
    public String name;
    @TreeNodeLevel
    public String level;
    ...
    getter setter
    ...
}

复制代码

使用注解对每个类型,其他类型类似

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeNodeId {
}
复制代码
Node 展示的数据类型
public class Node {
    // 自身id
    private String id;
    // 父辈id
    private String pid;
    // 显示内容
    private String name;
    // 条目的icon
    private int icon;
    // 树的层级
    private int level;
    //    是否展开
    private boolean isExpand;
    //    是否选中
    private boolean isChoose;
    // 在搜索查询的时候使用:判断本节点是否显示
    private boolean isShow;
    //    父节点
    private Node parent;
    //    儿子节点集合
    private List<Node> children = new ArrayList<>();
}
复制代码

4. 在工具类TreeHelper中进行数据的处理

/**
 * 处理从后台拿到的数据
 * 1. bean -> node 使用注解和反射
 * 2. 给node设定关系,设置好父节点和子节点
 * 3. 根据关系设置节点的图标
 * 4. 过滤可展示的根节点
 * 5. 辅助工具方法:过滤出可见的节点,根据展开设置显示
 */
public class TreeHelper<T> {

    /**
     * 1. 将从服务器拿到的数据转化为Node类型
     * 使用注解和反射的原因:1. 服务器返回的数据的类型可以随意,只要注解正确
     *
     * @param mDatas 数据源
     * @param <T>    数据源的类型
     * @return 返回的数据
     */
    public static <T> List<Node> convertDates2Nodes(List<T> mDatas) {
        List<Node> nodes = new ArrayList<>();
        for (T t : mDatas) {
            String id = "";
            String pId = "";
            String lable = "";
            String level = "";
            Class clazz = t.getClass();
            Field[] fields = clazz.getDeclaredFields();
            try {
                for (Field field : fields) {
                    if (field.getAnnotation(TreeNodeId.class) != null) {
                        field.setAccessible(true);
                        id = (String) field.get(t);
                    }
                    if (field.getAnnotation(TreeNodePId.class) != null) {
                        field.setAccessible(true);
                        pId = (String) field.get(t);
                    }
                    if (field.getAnnotation(TreeNodeLable.class) != null) {
                        field.setAccessible(true);
                        lable = ((String) field.get(t));
                    }
                    if (field.getAnnotation(TreeNodeLevel.class) != null) {
                        field.setAccessible(true);
                        level = ((String) field.get(t));
                    }
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            Node node = new Node(id, pId, lable, Integer.parseInt(level));
            nodes.add(node);
        }
        // 指定 node 之间的父子关系
        for (int i = 0, len = nodes.size(); i < len; i++) {
            Node node1 = nodes.get(i);
            for (int j = i + 1; j < len; j++) {
                Node node2 = nodes.get(j);
                // node2 时 node1 的父亲
                if (node1.getPid().equals(node2.getId())) {
                    node1.setParent(node2);
                    node2.getChildren().add(node1);
                }
                // node1 时 node2 的父亲
                if (node2.getPid().equals(node1.getId())) {
                    node2.setParent(node1);
                    node1.getChildren().add(node2);
                }
            }
        }
        // 为每个节点设置图标
        for (Node node : nodes) {
            setNodeIcon(node);
        }
        return nodes;
    }


    /**
     * 如果当前节点有孩子节点并且展开状态, 设置向下的图标
     * 如果当前节点有孩子节点没有展开,设置向上的图标
     * 其他情况不设置图标
     *
     * @param node node
     */

    public static void setNodeIcon(Node node) {
        if (node.getChildren().size() > 0 && node.isExpand()) {
            node.setIcon(1);
        } else if (node.getChildren().size() > 0 && !node.isExpand()) {
            node.setIcon(0);
        } else {
            node.setIcon(-1);
        }
    }

    /**
     * 从所有的节点中过滤出根节点
     *
     * @param nodes nodes
     * @return 过滤之后的数据
     */
    private static List<Node> getRootResult(List<Node> nodes) {
        List<Node> root = new ArrayList<>();
        for (Node node : nodes) {
            if (node.isRoot()) {
                root.add(node);
            }
        }
        return root;
    }

    /**
     * 对节点进行排序
     *
     * @param datas
     * @param defaultExpandLevel
     * @param <T>
     * @return
     */
    public static <T> List<Node> getSortNodes(List<T> datas, int defaultExpandLevel) {
        List<Node> result = new ArrayList<>();
        List<Node> nodes = convertDates2Nodes(datas);
        List<Node> rootNodes = getRootResult(nodes);
        for (Node node : rootNodes) {
            // TODO:??????
            addNode(result, node, defaultExpandLevel, 1);
        }
        return result;
    }

    /**
     * 将一个节点的所有孩子节点都放入result中
     *
     * @param result
     * @param node               当前节点
     * @param defaultExpandLevel 默认初始化是展开几层
     * @param currentLevel       当前节点层级
     */
    public static void addNode(List<Node> result, Node node, int defaultExpandLevel, int currentLevel) {
        result.add(node);
        if (defaultExpandLevel > currentLevel) {
            node.setExpand(true);
        }
        if (node.isLeafNode()) {
            return;
        } else {
            for (int i = 0, len = node.getChildren().size(); i < len; i++) {
                addNode(result, node.getChildren().get(i), defaultExpandLevel, currentLevel + 1);
            }
        }
    }

    /**
     * 根据转化之后的列表,进行过滤可以展示的数据
     * @param mDatas
     * @return
     */
    public static List<Node> getVisibleNodes(List<Node> mDatas) {
        List<Node> result = new ArrayList<>();
        for (Node node : mDatas) {
            if (node.isRoot() || node.getParent().isExpand()) {
                result.add(node);
                if (node.getChildren() != null && node.getChildren().size() > 0) {
                    getVisibleNodes(node.getChildren());
                }
            }
        }
        return result;
    }
}
复制代码

5. Adapter 编写

TreeListViewAdapter 继承 BaseAdapter ,处理点击展开和收起,和一些基本的监听

public abstract class TreeListViewAdapter<T> extends BaseAdapter implements AdapterView.OnItemClickListener {

    protected List<Node> mAllNodes = new ArrayList<>();
    protected Context mContext;
    protected ListView listView;
    protected LayoutInflater inflater;
    protected List<Node> mVisibleNodes;
    protected int layoutId;
    protected int variabledId;

    protected OnTreeNodeClickListener onTreeNodeClickListener;

    public TreeListViewAdapter(Context context, ListView listView, List<T> datas, int layoutId, int variabledId, int defaultExpandLevel) {
        this.mContext = context;
        inflater = LayoutInflater.from(mContext);
        this.listView = listView;
        this.layoutId = layoutId;
        this.variabledId = variabledId;
        mAllNodes = TreeHelper.getSortNodes(datas, defaultExpandLevel);
        mVisibleNodes = TreeHelper.getVisibleNodes(mAllNodes);
        listView.setOnItemClickListener(this);
    }

    @Override
    public int getCount() {
        return mVisibleNodes.size();
    }

    @Override
    public Object getItem(int i) {
        return mVisibleNodes.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        Node node = mVisibleNodes.get(i);
        view = getConvertView(node, i, view, viewGroup);
        view.setPadding(40 * node.getLevel(), 4, 4, 4);
        return view;
    }


    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
        expandOrCollapse(i);
        if (onTreeNodeClickListener != null) {
            onTreeNodeClickListener.setOnClick(mVisibleNodes.get(i), i, view);
        }
    }

    /**
     * 设置点击展开或者收缩
     * @param position position
     */
    private void expandOrCollapse(int position) {
        Node node = mVisibleNodes.get(position);
        if (node != null) {
            if (node.isLeafNode()) {
                return;
            }
            node.setExpand(!node.isExpand());
            mVisibleNodes = TreeHelper.getVisibleNodes(mAllNodes);
            notifyDataSetChanged();
        }
    }

    public void setOnTreeNodeClickListener(OnTreeNodeClickListener onTreeNodeClickListener) {
        this.onTreeNodeClickListener = onTreeNodeClickListener;
    }

    public interface OnTreeNodeClickListener {
        void setOnClick(Node node, int position, View view);
    }
    public abstract View getConvertView(Node node, int position, View view, ViewGroup viewGroup);

}
复制代码

ExpandTreeListAdapter 具体的具有可展开的实现类,我们会注意到相应ViewHodler没有了。

public class ExpandTreeListAdapter<T> extends TreeListViewAdapter {

    public ExpandTreeListAdapter(Context context, ListView listView, List<T> datas,int layoutId, int variabledId, int defaultExpandLevel) {
        super(context, listView, datas, layoutId,variabledId, defaultExpandLevel);
    }

    @Override
    public View getConvertView(Node node, final int position, View convertView, ViewGroup viewGroup) {
        ItemListActivityLayoutBinding binding = null;
        if (convertView == null) {
            binding = DataBindingUtil.inflate(inflater, layoutId, viewGroup, false);
        } else {
            binding = DataBindingUtil.getBinding(convertView);
        }
        binding.setVariable(variabledId, mVisibleNodes.get(position));
        if (node.isExpand()) {
            binding.tvItemListText.setTextColor(Color.WHITE);
            binding.getRoot().setBackgroundColor(Color.RED);
        } else {
            binding.tvItemListText.setTextColor(Color.BLACK);
            binding.getRoot().setBackgroundColor(Color.WHITE);
        }
        return binding.getRoot();
    }

    /**
     * 返回所的node
     *
     * @return
     */
    public List<Node> getSelectedNodes() {
        List<Node> nodeList = new ArrayList<>();
        for (int i = 0, len = mAllNodes.size(); i < len; i++) {
            Node node = ((Node) mAllNodes.get(i));
            if (node.isChoose()) {
                nodeList.add(node);
            }
        }
        return nodeList;
    }

}

复制代码

到这里,整个的展示都处理完了,我们可以通过 mAdapter.setOnTreeNodeClickListener(); 进行UI上的变化。

注意几点:

  1. 如果数据源FileBean的结构和文章中添加的类型一样,以整个List展示,按照TreeHelper中的处理是可以的。
  2. 如果数据源FileBean的结构是下面的类型
        "code": "01",
        "name": "农业、园艺产品",
        "level": "1",
        "parentCode": "0",
        "children": [
          {
            "code": "0101",
            "name": "谷物",
            "level": "2",
            "parentCode": "01",
            "children": [
              {
                "code": "010101",
                "name": "稻谷",
                "level": "3",
                "parentCode": "0101",
                "children": [{
                    "code": "0101",
                    "name": "谷物",
                    "level": "2",
                    "parentCode": "01",
                    "children": []
                }]
              }]
             }
            ]
复制代码

TreeHelper 中增加方法convertDates2Nodes 先讲树形结构转化为List, 这样转化之后,之后再增加对数据处理的其他功能,就会比较简单。

   public static List<TreeBean> convertDates2Nodes(List<TreeBean> mDatas, int defaultExpandLevel) {
        List<TreeBean> result = new ArrayList<>();
        // 如何将树形存储的数据转化为list
        for (int i = 0; i < mDatas.size(); i++) {
            convert(result, mDatas.get(i), defaultExpandLevel, 1);
        }
        return result;
    }

    /**
     *
     * @param result
     * @param node
     * @param defaultExpandLevel
     * @param currentLevel
     */
    public static void convert(List<TreeBean> result, TreeBean node, int defaultExpandLevel, int currentLevel) {
            setNodeIcon(node);
            result.add(node);
            if (defaultExpandLevel > currentLevel) {
                node.setExpand(true);
            }
            if (node.isLeafNode()) {
                return ;
            } else {
                // 处理子节点
                for (int i = 0, len = node.getChildren().size(); i < len; i++) {
                    node.getChildren().get(i).setParent(node);
                    node.getChildren().add(node.getChildren().get(i));
                    convert(result, node.getChildren().get(i), defaultExpandLevel, node.getLevel() + 1);
                }
            }
    }
复制代码

转载于:https://juejin.im/post/5a41ed5bf265da431d3cee7e