ExpandableListView(可折叠列表)的基本使用

感谢你提到 ExpandableListView(可折叠列表)。在 Android 开发中,ExpandableListView 是一种支持分组和子项的控件,适合展示具有层级结构的数据,例如分类列表、树形菜单或设置页面。每个组可以展开或折叠以显示/隐藏子项。以下是对 ExpandableListView 的基本使用讲解,包括原理、步骤、代码示例及优化建议。如果你的需求涉及特定场景(例如自定义布局、动态数据、或复杂交互),请提供更多细节,我可以进一步定制答案。


1. ExpandableListView 简介

ExpandableListView 是一个继承自 ListView 的控件,用于展示两级数据结构:组(Group)子项(Child)。用户可以点击组标题来展开或折叠子项列表。

特点

  • 支持分组数据展示,适合层级结构。
  • 使用 ExpandableListAdapter(如 BaseExpandableListAdapter)提供数据和视图。
  • 支持组和子项的点击、长按事件。
  • 内置展开/折叠动画,性能适中。

常见用途

  • 设置页面(如 Android 系统设置)。
  • 分类列表(如商品分类、联系人分组)。
  • 树形数据展示(如文件浏览器)。

局限性

  • 仅支持两级结构(组和子项),不支持多级嵌套。
  • 性能和灵活性不如 RecyclerView,现代开发推荐使用 RecyclerView 的嵌套实现。

2. ExpandableListView 基本使用步骤

  1. 添加 ExpandableListView 到布局:在 XML 中定义控件。
  2. 准备数据:创建组和子项的数据结构(如 List 或 Map)。
  3. 创建 Adapter:实现 BaseExpandableListAdapter,定义组和子项的视图。
  4. 设置 ExpandableListView:绑定 Adapter,处理展开、折叠和点击事件。

3. 基本示例:使用 BaseExpandableListAdapter

以下是一个简单的 ExpandableListView 示例,展示分组的文本列表(例如城市分类),支持组和子项点击事件。

3.1 布局文件activity_main.xml

定义 ExpandableListView 控件。

3.2 组和子项布局

  • 组布局group_item_layout.xml):显示组标题。
  • 子项布局child_item_layout.xml):显示子项内容。

3.3 自定义 AdapterCustomExpandableListAdapter.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 自定义 AdapterCustomExpandableListAdapter.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. 优化建议

  1. 性能优化
  • 使用 ViewHolder 模式(如上例)缓存组和子项视图。
  • 对于图片加载,使用异步库(如 Glide):
    java Glide.with(context) .load(item.getIconResId()) .thumbnail(0.25f) .placeholder(R.drawable.placeholder) .into(holder.iconView);
  1. 动态数据更新
  • 添加组或子项:
    java public void addGroup(GroupItem group, List<ChildItem> children) { groupList.add(group); childMap.put(group, children); notifyDataSetChanged(); }
  1. 防止内存泄漏
  • 使用 ApplicationContext(如上例)。
  • onDestroy 中清理引用。
  1. 状态管理
  • 如果子项包含 CheckBox,参考前文“CheckBox 错位问题”:
    • ChildItem 中存储 isChecked
    • getChildView 中同步状态并重置监听器。
  1. 展开状态保存
  • 保存展开的组:
    java SparseBooleanArray expandedGroups = new SparseBooleanArray(); expandableListView.setOnGroupExpandListener(groupPosition -> expandedGroups.put(groupPosition, true)); expandableListView.setOnGroupCollapseListener(groupPosition -> expandedGroups.put(groupPosition, false));

7. ExpandableListView vs RecyclerView

特性ExpandableListViewRecyclerView (Nested)
层级支持两级(组+子项)支持多级嵌套
性能较低(无强制 ViewHolder)较高(强制 ViewHolder)
数据更新全量刷新 (notifyDataSetChanged)支持局部刷新 (notifyItemChanged)
灵活性有限高(支持复杂布局和动画)
现代性较老旧推荐使用

建议:对于多级嵌套或复杂交互,推荐使用 RecyclerView 结合嵌套适配器或 ConcatAdapter


8. 可能的其他意图

  • 复杂布局:如果需要更复杂的组/子项布局(例如包含 CheckBox、按钮),请提供细节。
  • 交互需求:如果需要动态更新、异步加载或多选,请说明。
  • 数据可视化:如果需要将列表数据以图表形式展示(例如组或子项统计),我可以生成 Chart.js 图表,但需要数据。
  • 跨平台需求:如果需要 iOS 或 Web 的折叠列表方案,请说明。
  • 问题调试:如果有具体问题(例如性能、状态错乱),请描述。

下一步

请提供更多细节,例如:

  • 你需要的组/子项布局(简单文本、图片+文本、或更复杂)?
  • 是否需要交互(点击、CheckBox、动态更新)?
  • 是否需要多级嵌套或迁移到 RecyclerView
  • 是否有性能或其他问题?

如果没有进一步信息,我可以提供更复杂的 ExpandableListView 示例(例如包含 CheckBox 或多布局)或等效的 RecyclerView 实现。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注