# 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,先上图,看一下效果:

## 准备测试数据
列表数据分为组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)