# ExpandableDemo **Repository Path**: qiang129/ExpandableDemo ## Basic Information - **Project Name**: ExpandableDemo - **Description**: 可扩展的列表Demo - **Primary Language**: Android - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2022-03-03 - **Last Updated**: 2022-03-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 效果图 ExpandableListView继承于ListView,先上图,看一下效果: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200329185637933.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NlYV9ibw==,size_16,color_FFFFFF,t_70#pic_center) ## 准备测试数据 列表数据分为组item部分和子item部分,它们都有文字和icon组成,这里在assets文件夹下创建一个json文件取名ExpandableMode.json,用于存放这些测试,数据结构如下: ``` [ { "GroupName": "水果", "GroupIcon": "icon_fruit", "ChildData": [ { "ChildName": "香蕉", "ChildIcon": "icon_banana", "Number": 5 }, { "ChildName": "苹果", "ChildIcon": "icon_apple", "Number": 3 }, { "ChildName": "西瓜", "ChildIcon": "icon_watermelon", "Number": 9 } ] }, { "GroupName": "蔬菜", "GroupIcon": "icon_vegetable", "ChildData": [ { "ChildName": "萝卜", "ChildIcon": "icon_radish", "Number": 4 }, { "ChildName": "西红柿", "ChildIcon": "icon_tomatoes", "Number": 7 }, { "ChildName": "榴莲", "ChildIcon": "icon_durian", "Number": 9 } ] }, { "GroupName": "交通工具", "GroupIcon": "icon_traffic", "ChildData": [ { "ChildName": "汽车", "ChildIcon": "icon_car", "Number": 4 }, { "ChildName": "飞机", "ChildIcon": "icon_plane", "Number": 7 }, { "ChildName": "大炮", "ChildIcon": "icon_cannon", "Number": 9 }, { "ChildName": "火箭", "ChildIcon": "icon_rocket", "Number": 1 } ] } ] ``` 创建一个和json文件相对应的实体类ExpandableMode ```java package top.zhaohaibo.expandable.mode; import java.util.List; /** * 作者: Ocean

* 时间: 2020/3/28 9:57 PM

* 邮箱: xinzhaohaibo@aliyun.com

* 描述: 可展开视图的数据模型 */ public class ExpandableMode { /** * GroupName : 水果 * GroupIcon : * ChildData : [{"ChildName":"香蕉","ChildIcon":"","Number":5},{"ChildName":"苹果","ChildIcon":"","Number":3},{"ChildName":"西瓜","ChildIcon":"","Number":9}] */ private String GroupName; private String GroupIcon; private List ChildData; public String getGroupName() { return GroupName; } public void setGroupName(String GroupName) { this.GroupName = GroupName; } public String getGroupIcon() { return GroupIcon; } public void setGroupIcon(String GroupIcon) { this.GroupIcon = GroupIcon; } public List getChildData() { return ChildData; } public void setChildData(List ChildData) { this.ChildData = ChildData; } public static class ChildDataBean { /** * ChildName : 香蕉 * ChildIcon : * Number : 5 */ private String ChildName; private String ChildIcon; private int Number; public String getChildName() { return ChildName; } public void setChildName(String ChildName) { this.ChildName = ChildName; } public String getChildIcon() { return ChildIcon; } public void setChildIcon(String ChildIcon) { this.ChildIcon = ChildIcon; } public int getNumber() { return Number; } public void setNumber(int Number) { this.Number = Number; } } } ``` ## ExpandableListView适配器编写 这里自定义MyExpandableAdapter适配器继承与BaseExpandableListAdapter,并实现它的10个方法,基本上和ListView的适配器方法相同,可以看一下代码的注释,很详细的,不过有几个方法需要注意一下: ##### getGroupView()方法 getGroupView()是重要重写的方法之一,用于获取给定分组的视图,它4个参数,其中第一个参数isExpanded表示组item是否处于展开状态,通过这个参数可以切换右侧的箭头图标。实现添加组item布局,通过groupPosition获取ExpandableMode实体对象,设置icon和名字,根据组item的展开状态设置右边箭头。getGropVie完整代码如下: ``` /** * 获取给定分组的视图,主要重写的方法。 * * @param groupPosition groupPosition * @param isExpanded 该组是展开状态还是收起状态 * @param convertView convertView * @param parent parent * @return View */ @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_group, parent, false); } // 获取组的实体对象 ExpandableMode expandableMode = mExpandableModeList.get(groupPosition); // 获取组名 String groupName = expandableMode.getGroupName(); // 获取组icon名字 String iconName = expandableMode.getGroupIcon(); // 调用getImageMipmapRes方法获取对应的icon资源 int iconRes = getImageMipmapRes(mContext, iconName); // 获取Drawable对象 Drawable drawableLeft = ContextCompat.getDrawable(mContext, iconRes); mViewHolder.setImage(convertView, R.id.item_group_icon_iv, drawableLeft); // 设置TextView的文字 mViewHolder.setText(convertView, R.id.item_group_name_tv, groupName); // 设置组item展开或者关闭的右图片 Drawable drawableRight = ContextCompat.getDrawable(mContext, R.mipmap.icon_right); if (isExpanded) { // 展开 drawableRight = ContextCompat.getDrawable(mContext, R.mipmap.icon_down); } mViewHolder.setImageTextRight(convertView, R.id.item_group_name_tv, drawableRight); return convertView; } ``` 在设置icon时调用了getImageMipmapRes方法,getImageMipmapRes方法根据传入的图片名获取对应的图片资源: ``` /** * 通过文件名,获取对应的图片资源 * * @param context context * @param filename filename * @return 对应的图片资源 */ public int getImageMipmapRes(Context context, String filename) { return context.getResources().getIdentifier(filename, "mipmap", context.getPackageName()); } ``` ##### getChildView()方法 getChildView()方法用于添加子item布局,和getGroupView()方法类似: ``` /** * 获取给定分组给定子位置的数据用的视图,主要重写的方法。 * * @param groupPosition groupPosition * @param childPosition childPosition * @param isLastChild 是否为改组最后一个子视图 * @param convertView convertView * @param parent parent * @return View */ @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_child, parent, false); } ExpandableMode expandableMode = mExpandableModeList.get(groupPosition); List childDataBeans = expandableMode.getChildData(); ExpandableMode.ChildDataBean childDataBean = childDataBeans.get(childPosition); // 获取子名字 String childName = childDataBean.getChildName(); // 获取对应的子icon String childIcon = childDataBean.getChildIcon(); // 获取子icon名字对应的图片资源 int childIconRes = getImageMipmapRes(mContext, childIcon); // 获取Drawable对象 Drawable drawable = ContextCompat.getDrawable(mContext, childIconRes); // 设置icon mViewHolder.setImage(convertView, R.id.item_child_icon_iv, drawable); // 设置文字 mViewHolder.setText(convertView, R.id.item_child_name_tv, childName); return convertView; } ``` ##### isChildSelectable()方法 这个方法很重要它用来判断子item是否可以点击,如果它isChildSelectable返回false,子item将无法被点击,并且不会显示分割线,因此isChildSelectable应当设置返回true ``` /** * 子item是否可点击 * 当isChildSelectable方法返回false的时候,子item不可点击,且不会绘制分割线 * @param groupPosition groupPosition * @param childPosition childPosition * @return true可点击 false不可点击 */ @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } ``` #####hasStableIds()方法 这个用来判断ExpandableListView内容id是否有效的,我也不太明白它具体有什么作用,设置返回true和false的效果是一样的。 ``` /** * 用来判断ExpandableListView内容id是否有效的,我也不太明白,它具体有什么作用,设置返回true和false的效果是一样的。 * * @return true or false */ @Override public boolean hasStableIds() { return false; } ``` #### MyExpandableAdapter适配器的完整代码 MyExpandableAdapter适配器完整代码,ViewHolder请参适配器[张神万能的适配器](https://blog.csdn.net/lmj623565791/article/details/38902805/) ```java package top.zhaohaibo.expandable.adapter; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseExpandableListAdapter; import androidx.core.content.ContextCompat; import java.util.List; import top.zhaohaibo.expandable.R; import top.zhaohaibo.expandable.mode.ExpandableMode; import top.zhaohaibo.expandable.utils.ViewHolder; /** * 作者: Ocean

* 时间: 2020/3/28 9:37 PM

* 邮箱: xinzhaohaibo@aliyun.com

* 描述: 自定义可展开列表的适配器 */ public class MyExpandableAdapter extends BaseExpandableListAdapter { /** 数据集合 */ private List mExpandableModeList; /** 上下文 */ private Context mContext; /** ViewHolder */ private ViewHolder mViewHolder; public MyExpandableAdapter(List expandableModeList, Context context) { mExpandableModeList = expandableModeList; mContext = context; mViewHolder = ViewHolder.getInstance(); } /** * @return 返回组item数量 */ @Override public int getGroupCount() { return mExpandableModeList == null ? 0 : mExpandableModeList.size(); } /** * 根据出入的组groupPosition返回子item的数量。 * * @param groupPosition groupPosition * @return 返回子item的数量。 */ @Override public int getChildrenCount(int groupPosition) { if (mExpandableModeList == null) return 0; List childDataBeans = mExpandableModeList.get(groupPosition).getChildData(); return childDataBeans == null ? 0 : childDataBeans.size(); } /** * 获取组实体对象 * * @param groupPosition groupPosition * @return 返回组实体对象 */ @Override public Object getGroup(int groupPosition) { return mExpandableModeList == null ? null : mExpandableModeList.get(groupPosition); } /** * 获取子实体对象 * * @param groupPosition groupPosition * @param childPosition childPosition * @return 返回子实体对象 */ @Override public Object getChild(int groupPosition, int childPosition) { if (mExpandableModeList == null) return null; List childDataBeans = mExpandableModeList.get(groupPosition).getChildData(); return childDataBeans == null ? null : childDataBeans.get(childPosition); } /** * 获取组id,我理解为获取组的唯一ID,一般自己返回groupPosition即可 * * @param groupPosition groupPosition * @return Position */ @Override public long getGroupId(int groupPosition) { return 0; } /** * 获取子ID * * @param groupPosition groupPosition * @param childPosition childPosition * @return 返回子id */ @Override public long getChildId(int groupPosition, int childPosition) { return 0; } /** * 用来判断ExpandableListView内容id是否有效的,我也不太明白,它具体有什么作用,设置返回true和false的效果是一样的。 * * @return true or false */ @Override public boolean hasStableIds() { return false; } /** * 获取给定分组的视图,主要重写的方法。 * * @param groupPosition groupPosition * @param isExpanded 该组是展开状态还是收起状态 * @param convertView convertView * @param parent parent * @return View */ @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_group, parent, false); } // 获取组的实体对象 ExpandableMode expandableMode = mExpandableModeList.get(groupPosition); // 获取组名 String groupName = expandableMode.getGroupName(); // 获取组icon名字 String iconName = expandableMode.getGroupIcon(); // 调用getImageMipmapRes方法获取对应的icon资源 int iconRes = getImageMipmapRes(mContext, iconName); // 获取Drawable对象 Drawable drawableLeft = ContextCompat.getDrawable(mContext, iconRes); mViewHolder.setImage(convertView, R.id.item_group_icon_iv, drawableLeft); // 设置TextView的文字 mViewHolder.setText(convertView, R.id.item_group_name_tv, groupName); // 设置组item展开或者关闭的右图片 Drawable drawableRight = ContextCompat.getDrawable(mContext, R.mipmap.icon_right); if (isExpanded) { // 展开 drawableRight = ContextCompat.getDrawable(mContext, R.mipmap.icon_down); } mViewHolder.setImageTextRight(convertView, R.id.item_group_name_tv, drawableRight); return convertView; } /** * 获取给定分组给定子位置的数据用的视图,主要重写的方法。 * * @param groupPosition groupPosition * @param childPosition childPosition * @param isLastChild 是否为改组最后一个子视图 * @param convertView convertView * @param parent parent * @return View */ @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_child, parent, false); } ExpandableMode expandableMode = mExpandableModeList.get(groupPosition); List childDataBeans = expandableMode.getChildData(); ExpandableMode.ChildDataBean childDataBean = childDataBeans.get(childPosition); // 获取子名字 String childName = childDataBean.getChildName(); // 获取对应的子icon String childIcon = childDataBean.getChildIcon(); // 获取子icon名字对应的图片资源 int childIconRes = getImageMipmapRes(mContext, childIcon); // 获取Drawable对象 Drawable drawable = ContextCompat.getDrawable(mContext, childIconRes); // 设置icon mViewHolder.setImage(convertView, R.id.item_child_icon_iv, drawable); // 设置文字 mViewHolder.setText(convertView, R.id.item_child_name_tv, childName); return convertView; } /** * 子item是否可点击 * 当isChildSelectable方法返回false的时候,子item不可点击,且不会绘制分割线 * * @param groupPosition groupPosition * @param childPosition childPosition * @return true可点击 false不可点击 */ @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } /** * 通过文件名,获取对应的图片资源 * * @param context context * @param filename filename * @return 对应的图片资源 */ public int getImageMipmapRes(Context context, String filename) { return context.getResources().getIdentifier(filename, "mipmap", context.getPackageName()); } } ``` #### 组item的布局文件 ``` ``` #### 子item的布局文件 ``` ``` #### ViewHolder完整代码 ViewHolder完整代码如下: ```java package top.zhaohaibo.expandable.utils; import android.graphics.drawable.Drawable; import android.util.SparseArray; import android.view.View; import android.widget.ImageView; import android.widget.TextView; /** * 作者: Ocean

* 时间: 2020/3/28 9:57 PM

* 邮箱: xinzhaohaibo@aliyun.com

* 描述: ViewHolder * 见张神博客:https://blog.csdn.net/lmj623565791/article/details/38902805/ */ public class ViewHolder { /** * 静态内部类单例 */ private static class SingleHolder { private static final ViewHolder sViewHolder = new ViewHolder(); } /** * 获取SingleHolder实例 * * @return ViewHolder 对象 */ public static ViewHolder getInstance() { return SingleHolder.sViewHolder; } /** * 私有的构造方法,避免这个类在外部被实例化 */ private ViewHolder() { } /*** * 获取资源对象 * @param convertView 视图 * @param id 控件ID * @param 泛型 * @return 对应的视图类型 */ @SuppressWarnings("unchecked") public T get(View convertView, int id) { // 通过getTag获取控件view的标签,并强转成SparseArray对象,如果它为空这进行实例化,并把起设置成view的标签 SparseArray viewHolder = (SparseArray) convertView.getTag(); if (viewHolder == null) { viewHolder = new SparseArray<>(); convertView.setTag(viewHolder); } View childView = viewHolder.get(id); if (childView == null) { childView = convertView.findViewById(id); viewHolder.put(id, childView); } return (T) childView; } /** * 设置文字 * * @param convertView 布局View * @param childViewId 子控件id * @param s 内容 * @return ViewHolder */ public ViewHolder setText(View convertView, int childViewId, String s) { TextView textView = get(convertView, childViewId); textView.setText(s); return this; } /** * 设置文字 * * @param convertView 布局View * @param childViewId 子控件id * @param s 内容 * @return ViewHolder */ public ViewHolder setText(View convertView, int childViewId, int s) { TextView textView = get(convertView, childViewId); textView.setText(s); return this; } /** * 设置图片 * * @param convertView 布局View * @param childViewId 子控件id * @param drawable drawable * @return ViewHolder */ public ViewHolder setImage(View convertView, int childViewId, Drawable drawable) { ImageView imageView = get(convertView, childViewId); imageView.setImageDrawable(drawable); return this; } /** * 设置图文数据 * * @param convertView 布局View * @param childViewId 子控件id * @param txt 文本 * @param leftDrawable 左边图片 * @param rightDrawable 右图片 * @return ViewHolder ViewHolder */ public ViewHolder setImageText(View convertView, int childViewId, String txt, Drawable leftDrawable, Drawable rightDrawable) { TextView view = get(convertView, childViewId); if (isStrNull(txt)) { txt = ""; } view.setText(txt); view.setCompoundDrawablesWithIntrinsicBounds(leftDrawable, null, rightDrawable, null); return this; } /** * 设置文字的右图片 * * @param convertView 布局View * @param childViewId 子控件id * @return ViewHolder ViewHolder */ public ViewHolder setImageTextRight(View convertView, int childViewId, Drawable rightDrawable) { TextView view = get(convertView, childViewId); view.setCompoundDrawablesWithIntrinsicBounds(null, null, rightDrawable, null); return this; } /** * 空字符串判断 * * @param string 要判空的字符串 * @return true 空串, false非空 */ private boolean isStrNull(String string) { if (string == null || "null".equals(string) || "NULL".equals(string)) { return true; } // TextUtils.isEmpty(string); String str = string.replace(" ", ""); return str.length() == 0; } } ``` ## 主页面 主页面值有一个ExpandableListView控件, android:childDivider 设置子item的分割线, android:divider 设置分割线的颜色, android:dividerHeight 设置分割线的高度, android:groupIndicator 隐藏左边默认的的Icon。 布局文件如下: ``` ``` ##### ExpandableListView的数据刷新 ExpandableListView有两个重要方法, collapseGroup(groupPosition);关闭某一组。 expandGroup(groupPosition);展开某一组。 依次调用他们可以实现数据刷新: ``` // 子item的监听 mExpandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { Toast.makeText(MainActivity.this, childPosition + "", Toast.LENGTH_SHORT).show(); List childDataBeans = mExpandableModeList.get(groupPosition).getChildData(); childDataBeans.add(childDataBeans.get(childPosition)); // 设置数据 mExpandableAdapter.setExpandableModeList(mExpandableModeList); // 通过关闭组item在展开刷新数据 mExpandableListView.collapseGroup(groupPosition); mExpandableListView.expandGroup(groupPosition); return false; } }); ``` ##### 设置默认展开全部 ``` // 打开所有 for (int i = 0; i < mExpandableModeList.size(); i++) mExpandableListView.expandGroup(i); // 设置默认展开蔬菜组 // mExpandableListView.expandGroup(1); ``` #### 主页面完整代码 ```java package top.zhaohaibo.expandable; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.ExpandableListView; import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; import top.zhaohaibo.expandable.adapter.MyExpandableAdapter; import top.zhaohaibo.expandable.mode.ExpandableMode; /** * 作者: Ocean

* 时间: 2020/3/28 9:57 PM

* 邮箱: xinzhaohaibo@aliyun.com

* 描述: git pull origin master --allow-unrelated-histories */ public class MainActivity extends AppCompatActivity { /** 数据集合 */ private List mExpandableModeList; /** 适配器 */ private MyExpandableAdapter mExpandableAdapter; /** 可展开的List */ private ExpandableListView mExpandableListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mExpandableListView = findViewById(R.id.act_main_expandable_list_view); // 调用readAssetsFile方法读取assets下的测试数据 String ExpandableModeStr = readAssetsFile("ExpandableMode.json", this); Gson gson = new Gson(); // 通过Gson把读到的ExpandableModeStr转换为实体对象 mExpandableModeList = gson.fromJson(ExpandableModeStr, new TypeToken>() { }.getType()); // 实例化适配器 mExpandableAdapter = new MyExpandableAdapter(mExpandableModeList, this); // 设置适配器 mExpandableListView.setAdapter(mExpandableAdapter); // 子item的监听 mExpandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { Toast.makeText(MainActivity.this, childPosition + "", Toast.LENGTH_SHORT).show(); List childDataBeans = mExpandableModeList.get(groupPosition).getChildData(); childDataBeans.add(childDataBeans.get(childPosition)); // 设置数据 mExpandableAdapter.setExpandableModeList(mExpandableModeList); // 通过关闭组item在展开刷新数据 mExpandableListView.collapseGroup(groupPosition); mExpandableListView.expandGroup(groupPosition); return false; } }); // 打开所有 for (int i = 0; i < mExpandableModeList.size(); i++) mExpandableListView.expandGroup(i); // 设置默认展开蔬菜组 // mExpandableListView.expandGroup(1); } /** * 读取assets中的文件 流程 * readAssetsFile("json/xx.json", this); * * @param fileName 文件名 * @param context context * @return 读取的内容 */ public String readAssetsFile(String fileName, Context context) { // 输入流对象 InputStream inputStream; // 缓存流对象 BufferedReader bufferedReader = null; StringBuilder stringBuilder = new StringBuilder(); try { // getAssets方法返回通过输入流对象 inputStream = context.getAssets().open(fileName); // InputStreamReader 实现字节流到字符流的转换 bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line); } } catch (IOException e) { Log.e("MainActivity", e.toString()); } finally { try { if (bufferedReader != null) { bufferedReader.close(); } } catch (IOException e) { Log.e("MainActivity", e.toString()); } } return stringBuilder.toString(); } } ``` ## 源码 [Demo源码(喜欢点个星)](https://gitee.com/haibo2016/ExpandableDemo)