ExpandableListView(可折叠列表)的基本使用
感谢你提到 ExpandableListView(可折叠列表)。在 Android 开发中,ExpandableListView
是一种支持分组和子项的控件,适合展示具有层级结构的数据,例如分类列表、树形菜单或设置页面。每个组可以展开或折叠以显示/隐藏子项。以下是对 ExpandableListView
的基本使用讲解,包括原理、步骤、代码示例及优化建议。如果你的需求涉及特定场景(例如自定义布局、动态数据、或复杂交互),请提供更多细节,我可以进一步定制答案。
1. ExpandableListView 简介
ExpandableListView
是一个继承自 ListView
的控件,用于展示两级数据结构:组(Group) 和 子项(Child)。用户可以点击组标题来展开或折叠子项列表。
特点:
- 支持分组数据展示,适合层级结构。
- 使用
ExpandableListAdapter
(如BaseExpandableListAdapter
)提供数据和视图。 - 支持组和子项的点击、长按事件。
- 内置展开/折叠动画,性能适中。
常见用途:
- 设置页面(如 Android 系统设置)。
- 分类列表(如商品分类、联系人分组)。
- 树形数据展示(如文件浏览器)。
局限性:
- 仅支持两级结构(组和子项),不支持多级嵌套。
- 性能和灵活性不如
RecyclerView
,现代开发推荐使用RecyclerView
的嵌套实现。
2. ExpandableListView 基本使用步骤
- 添加 ExpandableListView 到布局:在 XML 中定义控件。
- 准备数据:创建组和子项的数据结构(如 List 或 Map)。
- 创建 Adapter:实现
BaseExpandableListAdapter
,定义组和子项的视图。 - 设置 ExpandableListView:绑定 Adapter,处理展开、折叠和点击事件。
3. 基本示例:使用 BaseExpandableListAdapter
以下是一个简单的 ExpandableListView
示例,展示分组的文本列表(例如城市分类),支持组和子项点击事件。
3.1 布局文件(activity_main.xml
)
定义 ExpandableListView
控件。
3.2 组和子项布局
- 组布局(
group_item_layout.xml
):显示组标题。 - 子项布局(
child_item_layout.xml
):显示子项内容。
3.3 自定义 Adapter(CustomExpandableListAdapter.java
)
实现 BaseExpandableListAdapter
,定义组和子项视图。
package com.example.myapp;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
import java.util.List;
import java.util.Map;
public class CustomExpandableListAdapter extends BaseExpandableListAdapter {
private Context context;
private List groupList;
private Map> childMap;
public CustomExpandableListAdapter(Context context, List<String> groupList, Map<String, List<String>> childMap) {
this.context = context.getApplicationContext();
this.groupList = groupList;
this.childMap = childMap;
}
@Override
public int getGroupCount() {
return groupList.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return childMap.get(groupList.get(groupPosition)).size();
}
@Override
public String getGroup(int groupPosition) {
return groupList.get(groupPosition);
}
@Override
public String getChild(int groupPosition, int childPosition) {
return childMap.get(groupList.get(groupPosition)).get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
GroupViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.group_item_layout, parent, false);
holder = new GroupViewHolder();
holder.textView = convertView.findViewById(R.id.groupText);
convertView.setTag(holder);
} else {
holder = (GroupViewHolder) convertView.getTag();
}
String group = getGroup(groupPosition);
holder.textView.setText(group);
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
ChildViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.child_item_layout, parent, false);
holder = new ChildViewHolder();
holder.textView = convertView.findViewById(R.id.childText);
convertView.setTag(holder);
} else {
holder = (ChildViewHolder) convertView.getTag();
}
String child = getChild(groupPosition, childPosition);
holder.textView.setText(child);
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true; // 子项可点击
}
static class GroupViewHolder {
TextView textView;
}
static class ChildViewHolder {
TextView textView;
}
}
3.4 Activity 代码(MainActivity.java
)
初始化数据并设置 ExpandableListView
。
package com.example.myapp;
import android.os.Bundle;
import android.widget.ExpandableListView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MainActivity extends AppCompatActivity {
private CustomExpandableListAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 准备数据
List<String> groupList = new ArrayList<>();
Map<String, List<String>> childMap = new HashMap<>();
groupList.add("华北");
groupList.add("华东");
groupList.add("华南");
List<String> huabeiCities = new ArrayList<>();
huabeiCities.add("北京");
huabeiCities.add("天津");
huabeiCities.add("石家庄");
List<String> huadongCities = new ArrayList<>();
huadongCities.add("上海");
huadongCities.add("杭州");
huadongCities.add("南京");
List<String> huananCities = new ArrayList<>();
huananCities.add("广州");
huananCities.add("深圳");
huananCities.add("珠海");
childMap.put("华北", huabeiCities);
childMap.put("华东", huadongCities);
childMap.put("华南", huananCities);
// 设置 ExpandableListView
ExpandableListView expandableListView = findViewById(R.id.expandableListView);
adapter = new CustomExpandableListAdapter(this, groupList, childMap);
expandableListView.setAdapter(adapter);
// 组点击事件
expandableListView.setOnGroupClickListener((parent, v, groupPosition, id) -> {
String group = groupList.get(groupPosition);
Toast.makeText(this, "点击组: " + group, Toast.LENGTH_SHORT).show();
return false; // 返回 false 允许展开/折叠
});
// 子项点击事件
expandableListView.setOnChildClickListener((parent, v, groupPosition, childPosition, id) -> {
String child = childMap.get(groupList.get(groupPosition)).get(childPosition);
Toast.makeText(this, "点击子项: " + child, Toast.LENGTH_SHORT).show();
return true;
});
// 展开/折叠事件
expandableListView.setOnGroupExpandListener(groupPosition -> {
String group = groupList.get(groupPosition);
Toast.makeText(this, "展开: " + group, Toast.LENGTH_SHORT).show();
});
expandableListView.setOnGroupCollapseListener(groupPosition -> {
String group = groupList.get(groupPosition);
Toast.makeText(this, "折叠: " + group, Toast.LENGTH_SHORT).show();
});
}
@Override
protected void onDestroy() {
super.onDestroy();
adapter = null;
expandableListView.setAdapter(null);
}
}
3.5 运行效果
- 显示一个包含“华北”、“华东”、“华南”三个组的列表。
- 点击组标题(如“华北”)展开显示子项(如“北京”、“天津”)。
- 点击子项或组标题弹出 Toast 显示内容。
- 展开/折叠时显示提示。
4. 自定义 ExpandableListView 示例:图片+文本
以下是一个更复杂的示例,组和子项包含图片和文本。
4.1 数据模型
定义组和子项的数据类。
package com.example.myapp;
public class GroupItem {
private String name;
private int iconResId;
public GroupItem(String name, int iconResId) {
this.name = name;
this.iconResId = iconResId;
}
public String getName() { return name; }
public int getIconResId() { return iconResId; }
}
package com.example.myapp;
public class ChildItem {
private String name;
private int iconResId;
public ChildItem(String name, int iconResId) {
this.name = name;
this.iconResId = iconResId;
}
public String getName() { return name; }
public int getIconResId() { return iconResId; }
}
4.2 布局文件
- 组布局(
group_item_layout.xml
):添加图标。 - 子项布局(
child_item_layout.xml
):添加图标。
4.3 自定义 Adapter(CustomExpandableListAdapter.java
)
支持图片+文本的组和子项。
package com.example.myapp;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import java.util.Map;
public class CustomExpandableListAdapter extends BaseExpandableListAdapter {
private Context context;
private List groupList;
private Map> childMap;
public CustomExpandableListAdapter(Context context, List<GroupItem> groupList, Map<GroupItem, List<ChildItem>> childMap) {
this.context = context.getApplicationContext();
this.groupList = groupList;
this.childMap = childMap;
}
@Override
public int getGroupCount() {
return groupList.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return childMap.get(groupList.get(groupPosition)).size();
}
@Override
public GroupItem getGroup(int groupPosition) {
return groupList.get(groupPosition);
}
@Override
public ChildItem getChild(int groupPosition, int childPosition) {
return childMap.get(groupList.get(groupPosition)).get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
GroupViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.group_item_layout, parent, false);
holder = new GroupViewHolder();
holder.iconView = convertView.findViewById(R.id.groupIcon);
holder.textView = convertView.findViewById(R.id.groupText);
convertView.setTag(holder);
} else {
holder = (GroupViewHolder) convertView.getTag();
}
GroupItem group = getGroup(groupPosition);
holder.iconView.setImageResource(group.getIconResId());
holder.textView.setText(group.getName());
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
ChildViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.child_item_layout, parent, false);
holder = new ChildViewHolder();
holder.iconView = convertView.findViewById(R.id.childIcon);
holder.textView = convertView.findViewById(R.id.childText);
convertView.setTag(holder);
} else {
holder = (ChildViewHolder) convertView.getTag();
}
ChildItem child = getChild(groupPosition, childPosition);
holder.iconView.setImageResource(child.getIconResId());
holder.textView.setText(child.getName());
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
static class GroupViewHolder {
ImageView iconView;
TextView textView;
}
static class ChildViewHolder {
ImageView iconView;
TextView textView;
}
}
4.4 Activity 代码(MainActivity.java
)
初始化数据并设置事件。
package com.example.myapp;
import android.os.Bundle;
import android.widget.ExpandableListView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MainActivity extends AppCompatActivity {
private CustomExpandableListAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 准备数据
List<GroupItem> groupList = new ArrayList<>();
Map<GroupItem, List<ChildItem>> childMap = new HashMap<>();
groupList.add(new GroupItem("华北", R.drawable.ic_launcher_foreground));
groupList.add(new GroupItem("华东", R.drawable.ic_launcher_foreground));
groupList.add(new GroupItem("华南", R.drawable.ic_launcher_foreground));
List<ChildItem> huabeiCities = new ArrayList<>();
huabeiCities.add(new ChildItem("北京", R.drawable.ic_launcher_foreground));
huabeiCities.add(new ChildItem("天津", R.drawable.ic_launcher_foreground));
huabeiCities.add(new ChildItem("石家庄", R.drawable.ic_launcher_foreground));
List<ChildItem> huadongCities = new ArrayList<>();
huadongCities.add(new ChildItem("上海", R.drawable.ic_launcher_foreground));
huadongCities.add(new ChildItem("杭州", R.drawable.ic_launcher_foreground));
huadongCities.add(new ChildItem("南京", R.drawable.ic_launcher_foreground));
List<ChildItem> huananCities = new ArrayList<>();
huananCities.add(new ChildItem("广州", R.drawable.ic_launcher_foreground));
huananCities.add(new ChildItem("深圳", R.drawable.ic_launcher_foreground));
huananCities.add(new ChildItem("珠海", R.drawable.ic_launcher_foreground));
childMap.put(groupList.get(0), huabeiCities);
childMap.put(groupList.get(1), huadongCities);
childMap.put(groupList.get(2), huananCities);
// 设置 ExpandableListView
ExpandableListView expandableListView = findViewById(R.id.expandableListView);
adapter = new CustomExpandableListAdapter(this, groupList, childMap);
expandableListView.setAdapter(adapter);
// 组点击事件
expandableListView.setOnGroupClickListener((parent, v, groupPosition, id) -> {
GroupItem group = groupList.get(groupPosition);
Toast.makeText(this, "点击组: " + group.getName(), Toast.LENGTH_SHORT).show();
return false;
});
// 子项点击事件
expandableListView.setOnChildClickListener((parent, v, groupPosition, childPosition, id) -> {
ChildItem child = childMap.get(groupList.get(groupPosition)).get(childPosition);
Toast.makeText(this, "点击子项: " + child.getName(), Toast.LENGTH_SHORT).show();
return true;
});
// 展开/折叠事件
expandableListView.setOnGroupExpandListener(groupPosition -> {
GroupItem group = groupList.get(groupPosition);
Toast.makeText(this, "展开: " + group.getName(), Toast.LENGTH_SHORT).show();
});
expandableListView.setOnGroupCollapseListener(groupPosition -> {
GroupItem group = groupList.get(groupPosition);
Toast.makeText(this, "折叠: " + group.getName(), Toast.LENGTH_SHORT).show();
});
}
@Override
protected void onDestroy() {
super.onDestroy();
adapter = null;
expandableListView.setAdapter(null);
}
}
4.5 运行效果
- 显示包含“华北”、“华东”、“华南”三个组的列表,每个组标题带图标。
- 点击组标题展开显示子项(如“北京”),子项包含图标和文本。
- 点击组或子项弹出 Toast,展开/折叠时也有提示。
5. ExpandableListView 常用属性
在 XML 中可以设置以下属性:
android:groupIndicator="@drawable/custom_indicator"
:自定义组展开/折叠指示器。android:divider="#CCCCCC"
:分隔线颜色。android:childDivider="#EEEEEE"
:子项分隔线颜色。
代码设置示例:
// 设置默认展开某个组
expandableListView.expandGroup(0);
// 禁用默认指示器
expandableListView.setGroupIndicator(null);
自定义指示器(res/drawable/custom_indicator.xml
):
6. 优化建议
- 性能优化:
- 使用
ViewHolder
模式(如上例)缓存组和子项视图。 - 对于图片加载,使用异步库(如 Glide):
java Glide.with(context) .load(item.getIconResId()) .thumbnail(0.25f) .placeholder(R.drawable.placeholder) .into(holder.iconView);
- 动态数据更新:
- 添加组或子项:
java public void addGroup(GroupItem group, List<ChildItem> children) { groupList.add(group); childMap.put(group, children); notifyDataSetChanged(); }
- 防止内存泄漏:
- 使用
ApplicationContext
(如上例)。 - 在
onDestroy
中清理引用。
- 状态管理:
- 如果子项包含
CheckBox
,参考前文“CheckBox 错位问题”:- 在
ChildItem
中存储isChecked
。 - 在
getChildView
中同步状态并重置监听器。
- 在
- 展开状态保存:
- 保存展开的组:
java SparseBooleanArray expandedGroups = new SparseBooleanArray(); expandableListView.setOnGroupExpandListener(groupPosition -> expandedGroups.put(groupPosition, true)); expandableListView.setOnGroupCollapseListener(groupPosition -> expandedGroups.put(groupPosition, false));
7. ExpandableListView vs RecyclerView
特性 | ExpandableListView | RecyclerView (Nested) |
---|---|---|
层级支持 | 两级(组+子项) | 支持多级嵌套 |
性能 | 较低(无强制 ViewHolder) | 较高(强制 ViewHolder) |
数据更新 | 全量刷新 (notifyDataSetChanged ) | 支持局部刷新 (notifyItemChanged ) |
灵活性 | 有限 | 高(支持复杂布局和动画) |
现代性 | 较老旧 | 推荐使用 |
建议:对于多级嵌套或复杂交互,推荐使用 RecyclerView
结合嵌套适配器或 ConcatAdapter
。
8. 可能的其他意图
- 复杂布局:如果需要更复杂的组/子项布局(例如包含 CheckBox、按钮),请提供细节。
- 交互需求:如果需要动态更新、异步加载或多选,请说明。
- 数据可视化:如果需要将列表数据以图表形式展示(例如组或子项统计),我可以生成 Chart.js 图表,但需要数据。
- 跨平台需求:如果需要 iOS 或 Web 的折叠列表方案,请说明。
- 问题调试:如果有具体问题(例如性能、状态错乱),请描述。
下一步
请提供更多细节,例如:
- 你需要的组/子项布局(简单文本、图片+文本、或更复杂)?
- 是否需要交互(点击、CheckBox、动态更新)?
- 是否需要多级嵌套或迁移到
RecyclerView
? - 是否有性能或其他问题?
如果没有进一步信息,我可以提供更复杂的 ExpandableListView
示例(例如包含 CheckBox 或多布局)或等效的 RecyclerView
实现。