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="" 他妈的居然也无法点击