7、ListView与Adapter:ArrayAdapter、SimpleAdapter、BaseAdapter,实现列表展示与点击事件
列表展示,几乎是每个App都绕不开的功能。从通讯录到设置页,从商品列表到聊天记录,底层都是ListView或它的升级版在干活。今天我们就来聊聊ListView和它的灵魂搭档——Adapter。
我个人习惯把Adapter理解成「数据的中转站」。它不生产数据,也不负责画界面,但它知道怎么把数据塞进ListView的每个格子里。说白了,没有Adapter,ListView就是个空壳子。
7.1 ListView的基本用法
先看一个最简单的例子。假设我们要展示一个水果列表:
// 在布局文件中添加ListView
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// 在Activity中绑定数据
ListView listView = findViewById(R.id.listView);
String[] fruits = {"苹果", "香蕉", "橙子", "葡萄", "西瓜"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
fruits
);
listView.setAdapter(adapter);
嗯,这里要注意:android.R.layout.simple_list_item_1是系统自带的布局,只有一个TextView。如果你觉得太单调,可以换成simple_list_item_2,它有两个TextView,适合展示标题和副标题。
7.2 三种Adapter的对比
Android提供了三种常用的Adapter,它们各有各的适用场景。我整理了一张表,方便你对比:
| Adapter类型 | 适用场景 | 数据源 | 灵活性 |
|---|---|---|---|
| ArrayAdapter | 简单文本列表 | 数组或List | 低 |
| SimpleAdapter | 键值对映射到多个控件 | List<Map<String, ?>> | 中 |
| BaseAdapter | 复杂自定义布局 | 任意类型 | 高 |
你想想看,如果只是显示几行文字,ArrayAdapter就够了。但如果你要展示一个带图片、按钮、多行文本的复杂列表,那就得用BaseAdapter自己动手了。
7.3 ArrayAdapter:最简单的选择
ArrayAdapter适合数据量小、结构简单的场景。我在项目中遇到过用它展示下拉菜单的选项列表,效果不错。
// 自定义布局的ArrayAdapter
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
R.layout.item_fruit, // 自定义布局
R.id.fruit_name, // 布局中TextView的id
fruits
);
这里有个坑:如果你用了自定义布局,记得在构造方法里传入TextView的id。否则Adapter找不到控件,列表会显示空白。
7.4 SimpleAdapter:键值对映射
SimpleAdapter适合数据来自Map的场景。比如从数据库或网络接口返回的数据,通常就是键值对形式。
List<Map<String, String>> data = new ArrayList<>();
for (String fruit : fruits) {
Map<String, String> map = new HashMap<>();
map.put("name", fruit);
map.put("desc", "新鲜水果");
data.add(map);
}
String[] from = {"name", "desc"};
int[] to = {R.id.fruit_name, R.id.fruit_desc};
SimpleAdapter adapter = new SimpleAdapter(
this,
data,
R.layout.item_fruit_detail,
from,
to
);
listView.setAdapter(adapter);
注意from和to数组的顺序必须一一对应。我曾经因为顺序搞反,调试了半天才发现是名字和描述显示反了。
7.5 BaseAdapter:终极武器
当列表项包含图片、按钮、多种字体样式时,BaseAdapter是唯一的选择。它需要你重写四个方法,但换来的是完全的控制权。
public class FruitAdapter extends BaseAdapter {
private Context context;
private List<Fruit> fruitList;
public FruitAdapter(Context context, List<Fruit> fruitList) {
this.context = context;
this.fruitList = fruitList;
}
@Override
public int getCount() {
return fruitList.size();
}
@Override
public Object getItem(int position) {
return fruitList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context)
.inflate(R.layout.item_fruit_complex, parent, false);
holder = new ViewHolder();
holder.ivIcon = convertView.findViewById(R.id.fruit_icon);
holder.tvName = convertView.findViewById(R.id.fruit_name);
holder.tvPrice = convertView.findViewById(R.id.fruit_price);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Fruit fruit = fruitList.get(position);
holder.ivIcon.setImageResource(fruit.getIcon());
holder.tvName.setText(fruit.getName());
holder.tvPrice.setText("¥" + fruit.getPrice());
return convertView;
}
static class ViewHolder {
ImageView ivIcon;
TextView tvName;
TextView tvPrice;
}
}
7.6 点击事件处理
列表展示只是第一步,用户点击才是交互的关键。ListView提供了三种点击监听:
// 1. 条目点击
listView.setOnItemClickListener((parent, view, position, id) -> {
Fruit fruit = (Fruit) parent.getItemAtPosition(position);
Toast.makeText(context, "点击了" + fruit.getName(), Toast.LENGTH_SHORT).show();
});
// 2. 条目长按
listView.setOnItemLongClickListener((parent, view, position, id) -> {
// 返回true表示消费事件,不再触发点击
return true;
});
// 3. 条目选中
listView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// 通常用于遥控器或键盘操作
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
这里有个细节:如果你在BaseAdapter的getView里给子控件(比如按钮)设置了点击事件,那么条目的点击事件可能不会被触发。解决办法是在子控件的点击事件里调用performClick(),或者干脆用onItemClickListener统一处理。
7.7 知识体系结构图
下面这张图梳理了ListView与Adapter的核心关系,以及三种Adapter的适用边界:
7.8 避坑指南
最后分享几个我踩过的坑:
- 数据更新后要通知Adapter: 调用
adapter.notifyDataSetChanged()。我曾经忘了这步,列表数据改了但界面没变,用户以为App卡死了。 - 不要在getView里做耗时操作: 比如网络请求、数据库查询。getView是在主线程执行的,会直接卡住UI。
- ListView的复用机制: convertView不为空时,一定要更新所有子控件的内容。否则会出现列表项显示错乱的问题。
- 大列表用RecyclerView: 如果数据量超过100条,建议改用RecyclerView。ListView在大量数据时性能会明显下降。
好了,关于ListView和Adapter的内容就聊到这里。这三种Adapter各有千秋,选对工具才能事半功倍。实际开发中,我80%的场景都在用BaseAdapter,因为它最灵活。但如果你只是展示简单的文本列表,ArrayAdapter绝对够用,别为了炫技而过度设计。
公众号:蓝海资料掘金营,微信deep3321