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,适合展示标题和副标题。

我的小经验: 在快速原型阶段,用系统自带的布局能省不少时间。但正式项目里,我建议还是自定义布局,毕竟UI风格要统一。

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);

注意fromto数组的顺序必须一一对应。我曾经因为顺序搞反,调试了半天才发现是名字和描述显示反了。

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;
    }
}
核心要点: ViewHolder模式一定要用。它避免了每次调用getView时都去findViewById,能显著提升列表滑动时的流畅度。我曾经在一个商品列表页里没加ViewHolder,结果在低端机上滑动卡成PPT。

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的适用边界:

ListView与Adapter知识体系 ListView ArrayAdapter SimpleAdapter BaseAdapter 简单文本列表 数组或List数据源 灵活性:低 键值对映射 List<Map>数据源 灵活性:中 完全自定义布局 任意数据源 灵活性:高 点击事件:OnItemClickListener

7.8 避坑指南

最后分享几个我踩过的坑:

  • 数据更新后要通知Adapter: 调用adapter.notifyDataSetChanged()。我曾经忘了这步,列表数据改了但界面没变,用户以为App卡死了。
  • 不要在getView里做耗时操作: 比如网络请求、数据库查询。getView是在主线程执行的,会直接卡住UI。
  • ListView的复用机制: convertView不为空时,一定要更新所有子控件的内容。否则会出现列表项显示错乱的问题。
  • 大列表用RecyclerView: 如果数据量超过100条,建议改用RecyclerView。ListView在大量数据时性能会明显下降。
特别注意: 如果你在BaseAdapter里使用了异步加载图片(比如Glide),一定要在convertView被回收时取消加载。否则图片会显示在错误的列表项上,用户体验极差。

好了,关于ListView和Adapter的内容就聊到这里。这三种Adapter各有千秋,选对工具才能事半功倍。实际开发中,我80%的场景都在用BaseAdapter,因为它最灵活。但如果你只是展示简单的文本列表,ArrayAdapter绝对够用,别为了炫技而过度设计。


公众号:蓝海资料掘金营,微信deep3321