ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android ListView几种用法简介:从ArrayAdapter到BaseAdapter的适配器实践

Android ListView几种用法简介:从ArrayAdapter到BaseAdapter的适配器实践 1. 为什么 ListView 的 Adapter 总让人卡在第一步Android 里的 ListView 是个老控件但它在很多存量项目、教学示例、嵌入式 Android 屏设备里依然活跃。你打开一个旧工程或者接手一个工业平板上的 App十有八九还能看到 ListView 配 Adapter 的写法。问题不在于 ListView 本身难而在于 Adapter 这个中间层把「数据」和「界面」隔开了初学者第一次看到getView(int i, View view, ViewGroup viewGroup)这种签名很容易懵这个 view 参数到底是新的还是旧的为什么要判断 nullViewHolder 又是干嘛的这篇内容就是围绕 Android ListView 的 Adapter 用法做一次梳理覆盖 ArrayAdapter、SimpleAdapter、BaseAdapter 三种典型写法。适合两类人一是刚学 Android、需要一份能直接跑起来的代码参考二是工作几年后回头复习想快速回忆每种 Adapter 的适用边界和坑点。我会给出可直接复制的布局 XML 和 Java 代码并说明怎么运行验证列表渲染、点击响应、数据刷新是否正常。先说结论性的判断ArrayAdapter 适合纯字符串或单一 TextView 的场景三行代码就能出效果SimpleAdapter 适合 Item 里有多个固定控件、数据是 Map 结构的场景不用写自定义类BaseAdapter 是万能方案Item 里有按钮、CheckBox、需要复用优化、需要动态刷新时必须用它。这三种不是替代关系而是复杂度递进的关系。你把它们放在同一个工程里对比着跑一遍比看十篇文章都管用。下面我会先讲清楚 ListView Adapter 的整体数据流再逐个拆解三种 Adapter 的完整代码最后给一份排错清单。所有代码都基于原生 Android SDK不依赖任何第三方库你新建一个 Empty Activity 工程就能跟着做。2. ListView 与 Adapter 的数据流先搞懂谁在调用谁在写代码之前有必要把 ListView 和 Adapter 的协作关系讲清楚。很多人写 Adapter 是「照着模板抄」一旦列表显示不对就不知道从哪查。其实整个链路只有四步ListView 测量自己需要显示多少个 Item然后向 Adapter 要数量拿到数量后ListView 只为屏幕上可见的那几个位置向 Adapter 要 ViewAdapter 负责把数据填进 View 再交回去用户滑动时滑出屏幕的 View 被回收ListView 再拿这些旧 View 向 Adapter 要新内容。这就是为什么getView的第一个参数view可能是 null也可能是一个已经创建过的旧 View。null 表示没有可复用的需要自己 inflate非 null 表示可以直接改内容复用。理解这一点后面 ViewHolder 的写法就顺理成章了。Adapter 的核心方法其实就四个。getCount()告诉 ListView 一共有多少条数据getItem(int position)返回对应位置的数据对象getItemId(int position)返回这一行的 id通常直接返回 positiongetView(int position, View convertView, ViewGroup parent)负责生成或复用每一行的 View。ArrayAdapter 和 SimpleAdapter 已经把前三个方法实现好了你只需要传数据BaseAdapter 是抽象类四个方法都得自己写。数据源的类型决定了你选哪种 Adapter。如果数据是String[]或ListStringArrayAdapter 最省事。如果数据是ListMapString, Object每个 Map 的 key 对应 Item 里一个控件的值SimpleAdapter 刚好匹配。如果数据是你自己定义的实体类比如ListUser而且 Item 布局里有多个控件甚至交互控件那就继承 BaseAdapter。还有一个容易被忽略的点ListView 的 Item 点击事件和 Item 内部控件的点击事件会冲突。如果你在 Item 里放了 ButtonButton 会抢走焦点导致 ListView 的onItemClick不触发。这也是为什么 SimpleAdapter 在带按钮的布局上不好用而 BaseAdapter 可以通过在根布局加android:descendantFocusabilityblocksDescendants来解决。这个坑后面排错部分会再展开。3. 三种 Adapter 的可复制配置与完整代码这一节是全文的核心我按「布局文件 → 数据准备 → 适配器绑定」的顺序把三种写法完整给出来。你可以新建一个工程把三个 ListView 放在同一个 Activity 里对比效果这样最直观。3.1 主布局与 ArrayAdapter 绑定字符串数组先建主布局activity_main.xml里面放三个 ListView分别对应三种 Adapter?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical ListView android:idid/lv_array android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / ListView android:idid/lv_simple android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / ListView android:idid/lv_base android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / /LinearLayoutArrayAdapter 的绑定最简单数据是字符串数组Item 直接用系统自带的android.R.layout.simple_list_item_1ListView lvArray findViewById(R.id.lv_array); String[] strDatas new String[]{first, second, third, fourth, fifth}; lvArray.setAdapter(new ArrayAdapter( this, android.R.layout.simple_list_item_1, strDatas));这里simple_list_item_1是 SDK 内置布局里面只有一个 TextViewid 是android.R.id.text1。ArrayAdapter 会自动把数组里的字符串塞进这个 TextView。如果你想让 Item 显示更复杂的内容可以传自定义布局但布局里必须有一个 id 为text1的 TextView否则 ArrayAdapter 找不到填充目标。这是 ArrayAdapter 的一个隐性约束很多人自定义布局后列表空白就是 id 写错了。3.2 SimpleAdapter 绑定 List与自定义 Item 布局SimpleAdapter 需要你先写一个 Item 布局。新建simple_adapter_item.xml里面放三个 TextView分别显示编号、姓名、电话?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding8dp TextView android:idid/sai_id android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:textSize16sp android:textColor#000000 / TextView android:idid/sai_name android:layout_width0dp android:layout_heightwrap_content android:layout_weight2 android:textSize16sp android:textColor#000000 / TextView android:idid/sai_phone android:layout_width0dp android:layout_heightwrap_content android:layout_weight3 android:textSize16sp android:textColor#000000 / /LinearLayout注意这里textSize用的是sp而不是dp。原示例里写的是18dp虽然能编译但字号应该用 sp这是 Android 的规范dp 用于尺寸sp 用于字体。这个细节在复习时值得留意。数据准备用ListMapString, Object每个 Map 的 key 和 Item 布局里的控件一一对应ListMapString, Object data new ArrayList(); String[] names {张三, 李四, 张二虎, 李老大, 王老二}; String[] phones {13588551201, 13588551202, 13588551203, 13588551204, 13588551205}; for (int i 0; i names.length; i) { MapString, Object map new HashMap(); map.put(id, String.format(%03d, i 1)); map.put(name, names[i]); map.put(phone, phones[i]); data.add(map); } ListView lvSimple findViewById(R.id.lv_simple); lvSimple.setAdapter(new SimpleAdapter( this, data, R.layout.simple_adapter_item, new String[]{id, name, phone}, new int[]{R.id.sai_id, R.id.sai_name, R.id.sai_phone}));SimpleAdapter 的构造参数里from数组是 Map 的 keyto数组是控件 id两者顺序必须一一对应。如果 key 写错或者控件 id 写错对应位置就是空白不会报错这是排查时最容易忽略的地方。3.3 BaseAdapter 继承实现与 ViewHolder 复用BaseAdapter 是重点。先定义一个数据实体类public class Data { public String id; public String name; public String phone; public Data(String id, String name, String phone) { this.id id; this.name name; this.phone phone; } }然后继承 BaseAdapter重写四个方法并在getView里用 ViewHolder 做复用public class MyAdapter extends BaseAdapter { private final ListData mDatas; private final LayoutInflater mInflater; public MyAdapter(Context context, ListData datas) { this.mDatas datas; this.mInflater LayoutInflater.from(context); } Override public int getCount() { return mDatas.size(); } Override public Object getItem(int position) { return mDatas.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 mInflater.inflate(R.layout.simple_adapter_item, parent, false); holder new ViewHolder(); holder.id convertView.findViewById(R.id.sai_id); holder.name convertView.findViewById(R.id.sai_name); holder.phone convertView.findViewById(R.id.sai_phone); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } Data item mDatas.get(position); holder.id.setText(item.id); holder.name.setText(item.name); holder.phone.setText(item.phone); return convertView; } static class ViewHolder { TextView id; TextView name; TextView phone; } }这里有两个细节值得说。第一inflate的第三个参数传parent, false而不是null。传 null 会导致 Item 的 layout 参数丢失宽高可能不对传 parent 但不 attach才能正确测量。第二ViewHolder 用 static 内部类避免持有外部类引用造成内存泄漏。这两点在面试和实际项目里都是高频考点。绑定数据和 ArrayAdapter 一样简单ListData datas new ArrayList(); datas.add(new Data(111, 老周1, 13656461200)); datas.add(new Data(112, 老周2, 13656461201)); datas.add(new Data(113, 老周3, 13656461202)); datas.add(new Data(114, 老周4, 13656461203)); datas.add(new Data(115, 老周5, 13656461204)); datas.add(new Data(116, 老周6, 13656461205)); ListView lvBase findViewById(R.id.lv_base); MyAdapter adapter new MyAdapter(this, datas); lvBase.setAdapter(adapter);如果你在 Item 布局里加了 Button 或 CheckBox记得在根布局加android:descendantFocusabilityblocksDescendants否则 ListView 的onItemClick不会触发。这是 BaseAdapter 相比 SimpleAdapter 的一个优势SimpleAdapter 无法处理 Item 内部控件的点击而 BaseAdapter 可以。4. 运行验证列表渲染、点击响应与数据刷新代码写完怎么确认它真的正常我一般分三步验证。第一步看渲染。运行 App三个 ListView 应该各占屏幕三分之一分别显示字符串、三列联系人、六行数据。如果某个 ListView 空白先检查setAdapter是否调用、数据集合是否为空、Item 布局的根节点宽高是否是wrap_content或match_parent。如果 Item 高度是 0列表看起来就是空的。第二步测点击。给 ListView 加OnItemClickListener点击时用 Toast 显示位置lvBase.setOnItemClickListener((parent, view, position, id) - { Data item datas.get(position); Toast.makeText(MainActivity.this, 点击了 item.name, Toast.LENGTH_SHORT).show(); });如果点击没反应先确认 Item 里没有抢焦点的控件如果有 Button就在根布局加blocksDescendants。另外onItemClick的position是数据位置不是 View 位置这点在数据有排序或过滤时要注意。第三步测刷新。BaseAdapter 的优势在这里体现修改数据集合后调用adapter.notifyDataSetChanged()列表会重新渲染。你可以加一个按钮点击后往datas里 add 一条再 notifyfindViewById(R.id.btn_add).setOnClickListener(v - { datas.add(new Data(117, 新用户, 13656461206)); adapter.notifyDataSetChanged(); });注意notifyDataSetChanged必须在主线程调用而且数据集合本身要是同一个引用。如果你重新赋值了datas new ArrayList()Adapter 里持有的还是旧引用刷新不会生效。这个坑我踩过不止一次。验证时还可以打开 Android Studio 的 Layout Inspector看 Item 的 View 是否被复用。滑动列表如果 View 数量稳定在屏幕可见数量加少量缓存说明 ViewHolder 生效了如果越滑越多说明复用逻辑有问题。5. 常见报错与排查清单这一节列几个真实会遇到的报错和现象对照着查能省不少时间。第一个是NullPointerException出现在getView里。最常见的原因是convertView.findViewById返回 null也就是 Item 布局里根本没有这个 id或者你 inflate 的布局文件写错了。检查R.layout.xxx和R.id.xxx是否对应。第二个是列表显示空白但没报错。ArrayAdapter 自定义布局时TextView 的 id 必须是android.R.id.text1写成自己的 id 就不会填充。SimpleAdapter 则是from和to数组长度不一致或者 key 拼写和 Map 里的不一致。第三个是java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification。这是你在非主线程改了数据集合或者改了数据但没调notifyDataSetChanged。解决办法是把数据修改放到主线程改完立即 notify。第四个是点击 Item 没反应。除了前面说的焦点问题还有一种情况是你给 Item 根布局设置了android:clickabletrue它会把点击事件消费掉。去掉这个属性或者改用onItemClick之外的方案。第五个是SimpleCursorAdapter报column _id does not exist。Cursor 作为数据源时必须包含_id列这是 Adapter 用来做 id 映射的。用MatrixCursor时构造参数里要显式加上_id。如果你在接入远程模型或 API 调试时遇到 401、local proxy failed、OAuth 回调失败这类问题那属于网络层排查和 ListView 无关但排查思路类似先确认配置项Base URL、Key、Model ID三件套是否齐全再看请求是否真的发出去了。TaoToken 的接入文档里有针对这些报错的对照说明需要的话可以去 https://taotoken.net/api 看接口说明或者在 https://taotoken.net/api-keys 生成 Key 后对照文档逐项核对。6. 复习之后怎么选三种 Adapter 的边界与延伸把三种写法跑通之后选型其实就一句话数据越简单用越省事的 Adapter需要交互和复用优化就上 BaseAdapter。ArrayAdapter 的边界是「单一文本」。只要 Item 里只有一个 TextView它是最优解。SimpleAdapter 的边界是「多个只读控件 Map 数据」一旦 Item 里有 Button、CheckBox、RatingBar 这类需要独立响应的控件它就不合适了。BaseAdapter 没有明显边界代价是代码量大四个方法都得自己维护。如果你在复习时想再往前走一步可以看看 RecyclerView。它是 ListView 的替代品把 ViewHolder 的职责内置了还支持局部刷新和多种 Item 类型。但 ListView 的这套 Adapter 思路是理解 RecyclerView 的基础尤其是getView的复用逻辑和 ViewHolder 模式在 RecyclerView 里换了个名字继续存在。所以先把这篇的三种写法吃透再去看 RecyclerView会顺很多。最后给一个实用建议把这三个 ListView 放在同一个工程里用同一份数据源分别绑定滑动对比它们的渲染差异。你会发现 ArrayAdapter 和 SimpleAdapter 在快速滑动时可能有轻微卡顿而 BaseAdapter 加了 ViewHolder 之后明显更顺。这个体感比任何文字描述都直观。代码可以直接复制上面的片段改改包名就能跑。
返回列表