尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android UI组件实战:AdapterView与ListView的Adapter适配全解析——从BaseAdapter到自定义列表项

Android UI组件实战:AdapterView与ListView的Adapter适配全解析——从BaseAdapter到自定义列表项 1. 为什么 AdapterView 家族总让人绕不过去Android 里凡是能滚动的列表背后几乎都站着 AdapterView 这一族。ListView、GridView、Spinner、Gallery 都继承自它而它们共同的特点就是自己不存数据只负责把 Adapter 递过来的东西画到屏幕上。你可以把 ListView 想成一个只会摆盘的桌子Adapter 才是那个决定每道菜长什么样、放什么内容的厨师。桌子本身不关心菜从哪来它只认厨师给的成品。这个设计带来的直接好处是数据源和视图彻底解耦。同一份 List 数据你可以用 ArrayAdapter 渲染成纯文字也可以换成 SimpleAdapter 渲染成图文混排甚至换成 BaseAdapter 做完全自定义的复杂布局ListView 一行都不用改。很多刚接触 Android 的朋友会卡在“为什么我 setAdapter 之后没反应”“为什么点击事件拿不到 position”这类问题上本质上都是没理清 Adapter 作为桥梁的职责边界。这篇内容面向需要快速搭建可滚动列表的移动端开发者重点放在 ListView 与 Adapter 的配合机制上。我会把 ArrayAdapter、SimpleAdapter、BaseAdapter 三种最常用的适配器拆开讲每个都给出可以直接复制进 Android Studio 的完整代码并且告诉你渲染结果长什么样、点击事件怎么接。读完你应该能独立判断什么场景用哪个 AdaptergetView 里为什么要用 ViewHolder以及列表滑动卡顿该往哪个方向查。需要说明的是AdapterView 的 XML 属性很多像android:choiceMode、android:divider、android:fastScrollEnabled这些它们控制的是选择行为、分隔条、快速滚动等表现层细节。这些属性我会在具体用到时顺带解释不会单独堆一大段属性表因为脱离场景记属性很容易忘。真正要刻进脑子的是 Adapter 的四个核心方法getCount、getItem、getItemId、getView理解了它们剩下的都是排列组合。2. 三种 Adapter 的选型与 ArrayAdapter 快速上手选 Adapter 其实就一句话数据越简单、布局越固定就用越省事的适配器。纯字符串列表用 ArrayAdapter图文键值对用 SimpleAdapter需要多控件组合、动态类型、复杂交互就用 BaseAdapter。下面逐个说。ArrayAdapter 是最省心的一个。它接受一个 Context、一个 item 布局资源、一个数组或 List。系统内置了几个现成的 item 布局最常用的是android.R.layout.simple_list_item_1就是一个 TextView。代码短到可以背下来private final String[] heroes {郭嘉, 荀攸, 荀彧, 程昱, 戏志才, 徐庶}; ArrayAdapterString adapter new ArrayAdapter( this, android.R.layout.simple_list_item_1, heroes); listView.setAdapter(adapter);如果你不想在 Java 里写数组也可以在 XML 里用android:entries直接绑一个数组资源。先在res/values/arrays.xml里定义string-array namebook item1/item item2/item item3/item item4/item /string-array然后 ListView 里写android:entriesarray/book连 setAdapter 都省了。这种方式适合静态配置项比如设置页面的选项列表。SimpleAdapter 稍微进阶一点它处理的是ListMapString, Object这种结构。构造方法五个参数Context、数据 List、item 布局 id、from 数组Map 里的 key、to 数组布局里控件的 id。它的绑定逻辑分两步先看有没有设置 ViewBinder如果 ViewBinder 的setViewValue返回 true 就交给它处理返回 false 才走默认绑定。默认绑定会按控件类型来CheckBox 这类 Checkable 期望布尔值TextView 期望字符串ImageView 期望资源 id 或字符串。如果类型对不上会直接抛IllegalStateException这是新手最容易踩的坑之一。BaseAdapter 是万能底座前面两个其实都间接或直接继承自它。你需要自己实现getCount、getItem、getItemId、getView四个方法。它的威力在于getView里你可以 inflate 任意布局配合 ViewHolder 缓存性能也能做到很好。后面我会给一个完整的图文列表例子。3. 可复制的完整配置从布局到 BaseAdapter这一节给一套能直接跑起来的代码。场景是一个带图标和文字的列表用 BaseAdapter 实现同时演示 ViewHolder 缓存和点击事件。先建 item 布局res/layout/list_item_icon_text.xml?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:gravitycenter_vertical android:padding12dp ImageView android:idid/icon android:layout_width48dp android:layout_height48dp android:contentDescriptionstring/app_name / TextView android:idid/text android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:layout_marginStart12dp android:textSize16sp / /LinearLayout主布局res/layout/activity_main.xml?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_main android:layout_widthmatch_parent android:layout_heightmatch_parent android:divider#DDDDDD android:dividerHeight1dp android:cacheColorHint#00000000 / /LinearLayout注意android:cacheColorHint#00000000这是解决滑动时背景变黑的经典配置。如果你的列表背景是浅色滑动时出现黑色拖影加上这行基本就好了。自定义 Adapter 的完整代码package com.example.listdemo; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; public class HeroAdapter extends BaseAdapter { private final Context mContext; private final String[] mNames; private final int[] mIcons; public HeroAdapter(Context context, String[] names, int[] icons) { this.mContext context; this.mNames names; this.mIcons icons; } Override public int getCount() { return mNames null ? 0 : mNames.length; } Override public Object getItem(int position) { return mNames[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(mContext) .inflate(R.layout.list_item_icon_text, parent, false); holder new ViewHolder(); holder.icon convertView.findViewById(R.id.icon); holder.text convertView.findViewById(R.id.text); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } holder.icon.setImageResource(mIcons[position]); holder.text.setText(mNames[position]); return convertView; } static class ViewHolder { ImageView icon; TextView text; } }Activity 里绑定并加点击事件package com.example.listdemo; import android.os.Bundle; import android.widget.ListView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { private final String[] names {郭嘉, 荀攸, 荀彧, 程昱, 戏志才, 徐庶}; private final int[] icons { R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView findViewById(R.id.lv_main); HeroAdapter adapter new HeroAdapter(this, names, icons); listView.setAdapter(adapter); listView.setOnItemClickListener((parent, view, position, id) - Toast.makeText(MainActivity.this, 点击了 names[position], Toast.LENGTH_SHORT).show()); } }这套代码跑起来就是一个带图标和文字的列表点击每一项会弹 Toast 显示对应名字。ViewHolder 的作用是把 findViewById 的结果缓存到 convertView 的 tag 里避免每次 getView 都重新查找控件。列表项少的时候感觉不出来一旦上百项差距非常明显。4. 验证请求与渲染结果怎么确认 Adapter 真的生效了代码写完怎么确认列表真的渲染成功、点击事件真的接上了我一般分三步验证。第一步看渲染。运行 App如果列表是空的先检查getCount()返回值。可以在里面打一行日志Log.d(HeroAdapter, count mNames.length)如果输出 0说明数据源本身是空的跟 Adapter 没关系。如果 count 正常但界面还是空检查 item 布局的根节点高度是不是写成了0dp或者wrap_content被父容器压没了。第二步看点击。setOnItemClickListener要生效item 布局里的可点击控件不能抢焦点。比如 item 里放了 Button 或 CheckBox它们会默认获取焦点导致 ListView 的 item 点击失效。解决办法是在这些控件上加android:focusablefalse或者给根布局加android:descendantFocusabilityblocksDescendants。这个坑我在做设置页面时踩过当时列表项里放了 Switch结果整行点不动查了半天才发现是焦点问题。第三步看滑动性能。打开 Android Studio 的 Profiler或者简单点在getView里打日志看 convertView 是否为 null。正常情况下屏幕内可见的 item 数量是固定的滑动时 convertView 应该被复用日志里 null 只出现前几次。如果每次都是 null说明复用没生效多半是getView里没有正确使用 convertView 参数或者 item 布局类型不一致导致getItemViewType返回了不同的值。还有一个验证点是数据刷新。如果你在运行时改了数据源记得调用adapter.notifyDataSetChanged()否则界面不会更新。注意这个方法必须在主线程调用子线程里改数据后要runOnUiThread包一下。对于需要跨设备或跨环境调试的场景如果你在本地跑模型或做接口联调可以用 TaoToken 的模型对话功能快速验证返回结构地址是 https://taotoken.net/api 配合接入文档 https://taotoken.net/doc 能省不少排查时间。不过这是辅助手段核心还是把 Adapter 的四个方法理清楚。5. 常见报错排查从 IllegalStateException 到点击无响应这一节把几个高频报错和现象列出来对照着查。java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification。这个报错的意思是数据变了但没通知 ListView。解决办法是在修改数据源后调用notifyDataSetChanged()并且确保修改和通知在同一个线程。如果你在子线程改了 List一定要切回主线程再通知。java.lang.IllegalStateException出现在 SimpleAdapter 绑定阶段通常是类型不匹配。比如你把一个 int 资源 id 绑到了 TextView 上或者把字符串绑到了 ImageView 上。检查 from 和 to 的对应关系以及 Map 里 value 的实际类型。点击无响应。前面提过item 里有 Button、CheckBox、ImageView 设置了 clickable 都会抢焦点。给根布局加android:descendantFocusabilityblocksDescendants是最省事的做法。另外如果你在getView里给 convertView 设置了setOnClickListener也会覆盖 ListView 的 item 点击这种情况要么改用 item 点击要么在监听里自己处理 position。ArrayIndexOutOfBoundsException在 getView 里。多半是 position 超出了数据源长度常见于数据源在异步加载过程中被清空但 ListView 还在请求旧 position 的视图。解决办法是在getView开头加边界判断或者用notifyDataSetChanged前先确保数据源完整。列表滑动卡顿。除了 ViewHolder还要检查getView里有没有做耗时操作比如读文件、解析 JSON、加载大图。图片加载建议用 Glide 或 Coil 这类库它们自带缓存和生命周期管理。另外item 布局层级不要太深能用 ConstraintLayout 就别嵌套多层 LinearLayout。android:entries不生效。检查数组资源是否放在res/values/下文件名和name属性是否一致。如果 ListView 同时设置了android:entries和代码里setAdapter代码设置的会覆盖 XML 的。6. 把列表能力接到真实开发流里列表写完之后下一步通常是接真实数据。本地调试阶段你可以先用假数据把 UI 跑通确认 Adapter 逻辑没问题再换成网络请求或数据库查询。网络请求回来的数据记得在主线程更新 UI数据库查询用 CursorLoader 或 Room 的异步查询避免阻塞主线程。如果你在做 AI 相关的移动端应用比如把模型返回的结果渲染成列表可以用 TaoToken 的 API Keys 管理密钥地址是 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc 。需要长期跑编码任务或 Agent 场景的话Coding Plan 页面 https://taotoken.net/coding-plan 有更详细的说明。这些工具能帮你把精力集中在 Adapter 和 UI 逻辑上而不是花在环境配置上。回到 ListView 本身它虽然被 RecyclerView 取代了很多场景但理解 Adapter 模式对用好 RecyclerView 的 Adapter 有直接帮助。两者核心思想一致数据与视图分离通过适配器桥接。把 ListView 这套吃透再去看 RecyclerView 的 ViewHolder、LayoutManager、ItemDecoration会顺很多。
返回列表