Android 的ExpandableListView使用总结--二级展开树结构

智数逐风使
• 阅读 7704

1、关于ExpandableListView的介绍

中文官方api--其实基本也不用怎么讲,直接看api也很清晰http://www.zhdoc.net/android/...

ExpandableListView 是默认支持二级展开树形结构,有的朋友喜欢用嵌套的方式实现多级的展开树,我并不建议那样用,写这篇文章就是单纯的总结一下这个空间,以及满足工作中只是简单的二级展开的需求。 后面我会再写一篇关于多层级的展开树,封装成自己的库使用。

2、ExpandableListView 使用

通过一个文件夹结构的例子来讲:
(1)创建布局文件,直接使用ExpandableListView

<?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="match_parent"
    android:orientation="vertical">


    <ExpandableListView
        android:id="@+id/expand_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    </ExpandableListView>
</LinearLayout>

(2)创建ExpandableListView的适配器adapter
这里需要继承BaseExpandableListAdapter,然后实现它的方法
方法虽然挺多,但是好理解,看名字就能知道什么意思。这段代码只是个实例,下面贴出这个文件树结构的代码


public class ExpandListAdapter extends BaseExpandableListAdapter {
    private Context mContext;
    private List<Bean> folders;  //文件夹数据
    private List<List<Bean>> childData; //子文件数据
    public ExpandListAdapter(Context context,
                             List<Bean> folders, List<List<Bean>> childData) {
        this.mContext = context;
        this.folders = folders;
        this.childData = childData;
      
    }
    @Override
    public int getGroupCount() {
        return 0;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        return 0;
    }

    @Override
    public Object getGroup(int groupPosition) {
        return null;
    }

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        return null;
    }

    @Override
    public long getGroupId(int groupPosition) {
        return 0;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return 0;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }

    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        return null;
    }

    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        return null;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return false;
    }
}

完整的代码如下:

public class ExpandListAdapter extends BaseExpandableListAdapter {

    private Context mContext;
    private List<Bean> folders;  //文件夹数据
    private List<List<Bean>> childData; //子文件数据
    private SparseArray<ImageView> mIndicators;  //创建一个map来存储指示器,然后根据位置改变
    public ExpandListAdapter(Context context,
                             List<Bean> folders, List<List<Bean>> childData) {
        this.mContext = context;
        this.folders = folders;
        this.childData = childData;
        this.mIndicators=new SparseArray<>();
    }


    @Override //获取分组的个数(也就是这里的文件夹个数)
    public int getGroupCount() {
        return folders.size();
    }

    @Override//获取指定分组中子选项的个数
    public int getChildrenCount(int groupPosition) {
        return childData.get(groupPosition).size();
    }

    @Override//获取指定的分组数据
    public Object getGroup(int groupPosition) {
        return folders.get(groupPosition);
    }

    @Override //获取指定分组中指定子选项的数据
    public Object getChild(int groupPosition, int childPosition) {
        return childData.get(groupPosition).get(childPosition);
    }

    @Override//获取指定分组的ID, 这个ID必须是唯一的
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    @Override//获取子选项的ID, 这个ID必须是唯一的
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override//分组和子选项是否持有稳定的ID, 就是说底层数据的改变会不会影响到它们。
    public boolean hasStableIds() {
        return true;
    }

    @Override//获取显示指定分组的视图
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        GroupViewHolder groupViewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.expadn_list_layout, parent, false);
            groupViewHolder = new GroupViewHolder();
            groupViewHolder.tv_title = convertView.findViewById(R.id.group_title);
            groupViewHolder.iv_icon = convertView.findViewById(R.id.group_icon);
            convertView.setTag(groupViewHolder);
        }else{
            groupViewHolder= (GroupViewHolder) convertView.getTag();
        }
        groupViewHolder.tv_title.setText(folders.get(groupPosition).getName());
        //把要随着状态改变的imageView 指示器添加到集合里面
        mIndicators.put(groupPosition,groupViewHolder.iv_icon);
        //改变指示器展开或者关闭的显示
        setIndicator(groupPosition,isExpanded);
        return convertView;
    }

    @Override//获取显示指定分组中的指定子选项的视图
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
       ChildViewHolder childViewHolder;
        if (convertView==null){
            convertView=LayoutInflater.from(mContext).inflate(R.layout.expand_child_layout,parent,false);
            childViewHolder=new ChildViewHolder();
            childViewHolder.tvTitle=convertView.findViewById(R.id.child_title);
            convertView.setTag(childViewHolder);

        }else{
            childViewHolder= (ChildViewHolder) convertView.getTag();
        }
        childViewHolder.tvTitle.setText(childData.get(groupPosition).get(childPosition).getName());

        return convertView;
    }

    @Override//指定位置上的子元素是否可选中
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

    class GroupViewHolder {
        TextView tv_title;
        ImageView iv_icon;
    }

    class ChildViewHolder {
        TextView tvTitle;
    }
    //设置展开收起的指示器
    public void setIndicator(int position,boolean isExpanded){
        if (isExpanded){
            //从集合中取出指示器的imageview,改变图片的显示
            mIndicators.get(position).setImageResource(R.mipmap.file_enter_icon);
        }else{
            mIndicators.get(position).setImageResource(R.mipmap.file_enter_icon_down);
        }
    }
}

代码很简单,这里讲一下过程
1、创建adapter类继承BaseExpandableListAdapter ,实现它的方法
2、根据传入的数据填充方法:这里传入了两个集合,一个是文件夹的集合,也就是这个树结构有多少个group;还有一个是元素是结合的集合,存储的是哪一个文件夹下的数据,也就是通过它来取出哪一组,然后从取出的组再取出组里的child
3、跟使用ListView一样,创建Viewholder,然后在getGroupView()和getChildView()这两个方法中找到相应的布局,声明相应的控件。然后优化一下性能方面,具体步骤看代码,和listView一模一样。
4、自定义指示器:(默认的实在是太丑了)
这个也很简单,就是上面文件夹布局(group)中添加一个imageview,具体添加在哪里,看个人喜好,我加载最右边了。通过展开、合上来动态修改显示的图片,待会再看代码,这里只是了解过程
5、为ExpandableListView设置适配器以及隐藏掉默认的指示器

ExpandListAdapter adapter=new ExpandListAdapter(this,folders,childData);
expandList.setGroupIndicator(null);//把指示器设为null
expandList.setAdapter(adapter);  //设置adapter

6、这个是硬加的,别忘了要创造数据,这里造假数据。我是创建了一个Bean类

 //初始化数据,由于没有后台接口,我们自己造假数据。开发中根据情况自己获取就可以
    //模拟文件夹
    private void initData() {
        folders = new ArrayList<>();
        folders.add(new Bean("文件夹一"));
        folders.add(new Bean("文件夹二"));
        folders.add(new Bean("文件夹三"));
        folders.add(new Bean("文件夹四"));

        childData = new ArrayList<>();
        List<Bean> list = new ArrayList<>();
        list.add(new Bean("A-01.1 Siteplan"));
        list.add(new Bean("A-01.2 Basement"));
        list.add(new Bean("A-01.3 楼层图"));
        list.add(new Bean("A-01.4 First floor"));
        childData.add(list);
        List<Bean> list1 = new ArrayList<>();
        list1.add(new Bean("A-02.1 图纸一"));
        list1.add(new Bean("A-02.2 图纸二"));
        childData.add(list1);
        List<Bean> list2 = new ArrayList<>();
        list2.add(new Bean("A-03.1 三楼图纸"));
        list2.add(new Bean("A-03.2 floor"));
        childData.add(list2);
        List<Bean> list3 = new ArrayList<>();
        list3.add(new Bean("A-04.1 pager1"));
        list3.add(new Bean("A-04.2 pager2"));
        list3.add(new Bean("A-04.3 pager3"));
        list3.add(new Bean("A-04.4 pager4"));
        childData.add(list3);

    }

Bean类如下:

public class Bean {
    private String name;

    public Bean() {
    }

    public Bean(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

通过以上的步骤我们基本上就已经实现了二级展开的树结构。

最后把没说完的讲完: 自定义指示器

private SparseArray<ImageView> mIndicators;  
//创建一个map来存储指示器,然后根据位置改变
//其实就是在走getGroupView的方法时每次更新时把item的指示器存入,
//然后在根据位置取出来,根据当时的状态来动态改变图片

我是在构造方法中进行初始化的。这个无所谓。顺便贴一下把

  public ExpandListAdapter(Context context,
                             List<Bean> folders, List<List<Bean>> childData) {
        this.mContext = context;
        this.folders = folders;
        this.childData = childData;
        this.mIndicators=new SparseArray<>();//初始化
    }

然后创建一个方法,来根据位置和展开状态来动态更新图片

 //设置展开收起的指示器
    public void setIndicator(int position,boolean isExpanded){
        if (isExpanded){
            //从集合中取出指示器的imageview,改变图片的显示
            mIndicators.get(position).setImageResource(R.mipmap.file_enter_icon);
        }else{
            mIndicators.get(position).setImageResource(R.mipmap.file_enter_icon_down);
        }
    }

最后千万别忘了一个最重要的操作:
在getGroupView方法中,要把imageView加到集合中,然后调用setIndicator()方法
那一块的代码我也贴一下:

  @Override//获取显示指定分组的视图
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        GroupViewHolder groupViewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.expadn_list_layout, parent, false);
            groupViewHolder = new GroupViewHolder();
            groupViewHolder.tv_title = convertView.findViewById(R.id.group_title);
            groupViewHolder.iv_icon = convertView.findViewById(R.id.group_icon);
            convertView.setTag(groupViewHolder);
        }else{
            groupViewHolder= (GroupViewHolder) convertView.getTag();
        }
        groupViewHolder.tv_title.setText(folders.get(groupPosition).getName());
        //把要随着状态改变的imageView 指示器添加到集合里面
        mIndicators.put(groupPosition,groupViewHolder.iv_icon);
        //改变指示器展开或者关闭的显示
        setIndicator(groupPosition,isExpanded);
        return convertView;
    }

3、点击事件

对于处理 Item 的点击事件,还是要设置监听器,常用的有这几类:
setOnChildClickListener()
setOnGroupClickListener()
setOnGroupCollapseListener()
setOnGroupExpandListener()
它们分别设置单击子选项、单击分组项、分组合并、分组展开的监听器。
这个不讲了,具体方法怎么用,直接看顶部的api文档,中文的,啥都有

4、随便聊聊

关于展开、合并的指示器,可以把setIndicator方法的调用放在点击事件里面。
每次点击的时候去实现展开关闭的操作
如果重写点击事件方法,他是默认展开的。。如果我们把return false 变为return true,而不给他指定操作,他就不会展开了。。返回true应该是表示这个点击事件我来做处理,处理完了,但是什么没有做,所以要自己写操作

 expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
            @Override
            public boolean onGroupClick(ExpandableListView expandableListView, View view, int i, long l) {
                boolean groupExpanded = expandableListView.isGroupExpanded(i);
                if (groupExpanded) {
                    expandableListView.collapseGroup(i);

                } else {
                    expandableListView.expandGroup(i, true);
                }

               adapter.setIndicatorState(i, !groupExpanded);
                Log.d("测试", "点击事件方法调用了");
                return true;

            }
        });

下面说的大家不用看,我只是自己记录下
补充一下:我写过一个文件夹的需求,文件夹的图片有四种,选中、不选中、展开并选中、展开不选中--

1、//创建一个集合存储组文件状态
private SparseArray<Boolean> expaned;
expaned=new SparseArray<>();

2、//在getGroupView 方法中把位置和状态存入
expaned.put(i,isExpanded);

3、在修改指示器的方法中加入判断
 //            根据分组的展开闭合状态设置指示器
    public void setIndicatorState(int groupPosition, boolean isExpanded) {
            //先遍历用户操作,根据之前的状态改变指示器(比如:展开了,但没有选中)
        for (int i = 0; i <mIndicators.size(); i++) {
            if (expaned.get(i)) {

                mIndicators.get(i).setImageResource(R.drawable.foder_open);

            } else {
                mIndicators.get(i).setImageResource(R.drawable.foder_icon);
            }
        }
        //然后根据点击事件传过来的状态,改变当前item的指示器样式 
        
        if (isExpanded) {

            mIndicators.get(groupPosition).setImageResource(R.drawable.foder_icon_open);

        } else {
            mIndicators.get(groupPosition).setImageResource(R.drawable.foder_icon);
        }


    }
    
    这个方法在点击事件中调用。每次都会遍历一遍存入的集合,然后根据状态更新一下。选中不选中。展开不展开

后来使用的天坑 记录下----浪费了很长时间
首先都知道子布局中如果有抢焦点的控件,比如Button,ExpandablelistView是无法点击的
如果是TextView ,设置了 inputType="" 他妈的居然也无法点击

点赞
收藏
评论区
推荐文章
android反调试源码实现
反调试的方法很多,不过由于android系统是开源的,所以反调试其实也不是很神秘的东西。下面是常见的也是很多厂商都在使用,包括我们项目组也在使用的。多个方案相互结合可以实现更好反调试。1.1ptrace自己,使得android_server附加不上javavoidanti_ptrace(){ptrace(PTRACE_TRACEME,0,
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
东方客主 东方客主
4年前
Android输入法遮挡了输入框,使用android:fitsSystemWindows="true"后界面顶部出现白条
问题1、页面布局文件:<LinearLayoutxmlns:android"http://schemas.android.com/apk/res/android"android:id"@id/layoutorderdetail"android:layoutwidth"matchparent"android:layoutheigh
AT大牛带你深度剖析Android 10大开源框架
做了几年的Android开发,也面试过不少公司,被面试过,也面试过不少人,其实Android的技术真的是无边界,不管你做过多牛的项目,不管你多久的经验在Android开发中不可能是停滞不前的,需要不断的学习及总结,否则难以解公关技术问题,下面把我压箱底的视频资料贡献给大家;一,android视频教程Android视频教程:1,Android入门
风斗 风斗
4年前
Android 通知栏使用
不同版本通知栏的创建方式不尽相同,当前官方推荐使用NotificationCompat相关的API,兼容到Android4.0,但是部分新功能,比如内嵌回复操作,旧版本是无法支持的。一、设置通知内容//CHANNEL_ID,渠道ID,Android8.0及更高版本必须要设置NotificationCompat.Builde
Stella981 Stella981
3年前
Android控件ListView简易使用(使用ArrayAdapter)
<?xmlversion"1.0"encoding"utf8"?<TextViewxmlns:android"http://schemas.android.com/apk/res/android"android:id"@id/tv"android:la
Wesley13 Wesley13
3年前
03.Android崩溃Crash库之ExceptionHandler分析
目录总结00.异常处理几个常用api01.UncaughtExceptionHandler02.Java线程处理异常分析03.Android中线程处理异常分析04.为何使用setDefaultUncaughtExceptionHandler前沿上一篇整体介绍了crash崩溃
Easter79 Easter79
3年前
SpringMvc接受特殊符号参数被转义
WEB开发时,在前端通过get/post方法传递参数的时候 如果实参附带特殊符号,后端接收到的值中特殊符号就会被转义例如该请求: http://localhost:10001/demo/index.do?name张三(1)注:中文()不会出现此种情况后台就收到的实际name值为:  张三&40;1&41;&40;其实为h
Stella981 Stella981
3年前
Android选项卡TabHost功能和用法
1、布局文件<TabHostxmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"android:id"@android:id/tabhost"
Wesley13 Wesley13
3年前
unity将 -u4E00 这种 编码 转汉字 方法
 unity中直接使用 JsonMapper.ToJson(对象),取到的字符串,里面汉字可能是\\u4E00类似这种其实也不用转,服务器会通过类似fastjson发序列化的方式,将json转对象,获取对象的值就是中文但是有时服务器要求将传参中字符串中类似\\u4E00这种转汉字,就需要下面 publ
Stella981 Stella981
3年前
Android 应用的动画实践
<h2id"menuIndex0"前言</h2<p尝试搜索了一下android动画的中文资料,很多都是一些枯燥的翻译api的一些文档,很少有系统讲解如何利用动画开发一个应用的资料,忽然,发现很多应用也不怎么注重动画在app的应用,想了想,自己尝试总结一下吧。因为,本人也不是什么动画制作师,没法把动画做得很绚丽,只好,利用内置的效果,进行简单加工
智数逐风使
智数逐风使
Lv1
不忍登高临远,望故乡渺邈,归思难收。
文章
4
粉丝
0
获赞
0