ListView — 用于显示垂直列表的基本 Android 组件,从 API 1(Android 1.0)开始可用。ListView 使用 ArrayAdapter 从元素创建列表,并支持自定义单元格布局。了解什么是 ListView、如何配置它以及这个古老但仍然使用的组件隐藏了哪些陷阱。在 IT Sectr,我们在遗留项目中遇到 ListView,并保持与 Android 4.4 的向后兼容性。要与现代替代方案进行比较,请阅读 关于 RecyclerView 的文章。
关键要点
ListView — 是来自 android.widget 包的 View 组,用于显示可滚动的元素列表。ListView 实现了 AdapterView 模式:数据通过适配器(Adapter)传递,适配器将数据元素转换为屏幕上显示的 View。ListView 从第一个版本(API 1)就存在于 Android SDK 中,不需要额外的库。
架构 ListView 比 RecyclerView 更简单:LayoutManager 是内置的(仅垂直滚动),没有更改动画,分隔器通过 XML 属性 android:divider 设置。ListView 自动添加 OverScrollView 和焦点高亮,简化了原型设计。根据 Statista(2025)的数据,Play Store 前 100 名中约 15% 的 Android 应用仍然包含 ListView — 主要是支持最少的遗留项目。
ListView 从 2008 年到 2014 年是 Android 中主要的列表组件,直到 Google I/O 2014 上推出了 RecyclerView。尽管处于部分弃用状态(soft-deprecated),ListView 仍然在 Android 的所有版本中可用,并且在 SDK API 35 中没有被标记为 @Deprecated。如果您的项目支持 Android 4.4(API 19)及更低版本,ListView 是唯一的内置选择,因为 RecyclerView 需要 AndroidX 和 gradle 依赖。
ArrayAdapter — 最流行的 ListView 适配器。它接受 List<T> 和每个元素的布局。默认情况下,ArrayAdapter 为每个对象调用 toString() 并在 simple_list_item_1(一个 TextView)中显示文本。对于自定义显示,需要重写 getView() 或在构造函数中传入自定义布局。
// 使用 ArrayAdapter 的 ListView(简单文本)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"俄罗斯", "美国", "德国",
"法国", "日本", "中国", "巴西"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
countries
)
binding.listView.adapter = adapter
// 点击处理
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter 用于来自 Map 数据的列表。它接受 List<Map<String, Any>> 和一个 from/to 数组,用于将 Map 键与布局中 widget 的 ID 绑定。SimpleAdapter 方便快速原型制作,当不需要单独的适配器类时。缺点 — 缺乏 type-safe 类型化和自定义逻辑的复杂性。
<!-- ListView 布局 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/divider"
android:dividerHeight="1px"
android:scrollbars="vertical" />
</LinearLayout>
自定义适配器 为 ListView 重写 getView() 方法,而 RecyclerView 将其分为 onCreateViewHolder + onBindViewHolder。在 ListView 中,这两个步骤是合并的,因此开发者必须通过 convertView(可复用的 View)和 setTag() 标签独立实现 ViewHolder 模式。
// 数据模型
data class Product(
name: String,
price: String,
imageResId: Int
)
// 带有 ViewHolder 的自定义适配器
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// 用于缓存的 ViewHolder
private class ViewHolder(view: View) {
val nameText: TextView = view.findViewById(R.id.tvName)
val priceText: TextView = view.findViewById(R.id.tvPrice)
val imageView: ImageView = view.findViewById(R.id.ivProduct)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.item_product, parent, false)
.also { it.tag = ViewHolder(it) }
val holder = view.tag as ViewHolder
val product = getItem(position)
holder.nameText.text = product.name
holder.priceText.text = product.price
holder.imageView.setImageResource(product.imageResId)
return view
}
}
与 RecyclerView 的关键区别:在 ListView 中,getView() 方法在元素每次出现在屏幕上时被调用。如果不使用 convertView 和 ViewHolder,每次滚动都会创建新的 View 对象,这会导致内存泄漏和 50 个以上元素列表的卡顿。带有 view.tag 的 ViewHolder — 对于具有自定义布局的 ListView 是强制优化。
ListView 有四个架构限制,Google 在 RecyclerView 中修复了这些限制:(1)仅垂直方向 — 没有网格、水平列表或级联;(2)缺乏点对点通知 — 任何数据更改都会通过 notifyDataSetChanged() 导致完整重绘,这会产生抖动;(3)没有内置的添加/删除元素动画;(4)没有职责分离 — LayoutManager 是内置的,ItemAnimator 不存在。
<!-- item_product.xml — ListView 元素布局 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivProduct"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/tvName"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPrice"
android:textSize="14sp"
android:textColor="@color/price" />
</LinearLayout>
</LinearLayout>
实际后果:在最多 20 个元素的列表上,ListView 和 RecyclerView 一样快。在 100 个以上元素时,差异变得明显:由于缺乏差异计算和强制 ViewHolder,ListView 开始丢失 FPS。对于频繁更新的列表(聊天、信息流、日志),ListView 不适合 — 请使用带有 Paging 3 的 RecyclerView。在 IT Sectr,我们在处理超过 50 个元素的遗留屏幕时,都会将 ListView 迁移到 RecyclerView。
ListView 提供了一组 XML 属性用于无需代码的快速配置:android:divider(分隔器的颜色/图片)、android:dividerHeight(厚度)、android:scrollbars(滚动条)、android:fastScrollEnabled(通过手柄快速滚动)、android:choiceMode(选择模式:singleChoice、multipleChoice)。android:entries 属性允许从资源数组(string-array)填充列表而无需适配器。
| 属性 | 描述 | 示例值 |
|---|---|---|
| android:divider | 分隔器的颜色或 drawable | @color/gray_light |
| android:dividerHeight | 分隔器的厚度(px/dp) | 1dp |
| android:entries | 来自资源的字符串数组 | @array/countries |
| android:choiceMode | 元素选择模式 | singleChoice |
| android:fastScrollEnabled | 快速滚动 | true |
| android:overlapAnchor | 弹出列表的锚点(API 21+) | true |
选择模式:choiceMode="singleChoice" 启用 RadioButton 样式,选中的元素被高亮。choiceMode="multipleChoice" — 多元素的复选框。选择数据可通过 listView.checkedItemPosition 或 listView.checkedItemIds 获取。重要提示:在 choiceMode 下,ListView 使用 StateListDrawable 作为背景 — 如果默认蓝色不符合设计,请设置自己的选择器。
选择 ListView 和 RecyclerView 取决于项目的需求。RecyclerView 是现代标准,但对于维护旧代码成本最低的简单屏幕,ListView 可能是合理的。所有关键参数的比较:
| 参数 | ListView | RecyclerView |
|---|---|---|
| 库大小 | 内置于 SDK(约 60 KB) | AndroidX(含依赖约 300 KB) |
| 最低版本 | API 1 | API 14(通过 AndroidX) |
| 方向 | 仅垂直 | 垂直、水平、网格、级联 |
| ViewHolder | 手动实现 | 架构强制要求 |
| 动画 | 仅淡入淡出过渡 | DefaultItemAnimator(添加、移除、移动、更改) |
| 点对点更新 | 无(仅 notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| 点击监听器 | onItemClickListener(内置) | 通过 ViewHolder(无内置) |
| 性能 | 对于最多 50 个元素的列表足够 | 1000+ 元素稳定 60 FPS |
结论:对于新项目,请使用 RecyclerView。仅在迁移成本不合理地高的遗留代码中保留 ListView。根据 Material Design 3(2026)指南,任何复杂度的列表都应使用 RecyclerView 以符合现代性能和可访问性标准。
常见问题
ListView 在 Android SDK API 35 中没有被标记为 @Deprecated,但 Google 不建议在新项目中(soft-deprecated)使用它。该组件在所有 Android 版本中仍然可用。RecyclerView 自 2014 年起成为官方替代品。如果您开始一个新项目 — 选择 RecyclerView。如果您维护现有项目 — ListView 将继续工作而无需更改。
ListView 支持 addHeaderView(View, Object, boolean) 和 addFooterView(View, Object, boolean) 方法。Header 和 footer 被添加到元素的总列表中,但不参与适配器 — 它们始终显示在列表的开头和结尾。重要提示:在设置适配器(setAdapter())之前添加 header/footer,否则它们不会被考虑。添加 header 后,元素位置的 ID 会偏移 header 的数量。
ListView 和 ScrollView 都处理垂直手势,这会产生冲突:外部 ScrollView 拦截滚动,而 ListView 无法滚动。解决方案:(1)不要将 ListView 放在 ScrollView 内 — 使用带有 NestedScrollingChild 的 RecyclerView,(2)将 ScrollView 替换为 NestedScrollView(API 21+),(3)在 ScrollView 内使用固定高度(android:layout_height="...dp")的 ListView,但这会破坏响应式布局。
ListView 中的数据清除取决于适配器类型。对于 ArrayAdapter:adapter.clear() 和 adapter.addAll(newList) 后跟 adapter.notifyDataSetChanged()。对于自定义适配器:更新数据列表并调用 notifyDataSetChanged()。注意:listView.invalidateViews() 只重绘 View,但不会更新数据。始终通过适配器更改数据,而不是直接操作。
ListView 元素的高度在 item 布局中设置。如果布局使用 layout_height="wrap_content",高度会根据内容调整。对于所有元素的固定高度,使用在 getView() 中设置 LayoutParams 的自定义适配器:view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight)。对于不同的高度,使用具有不同视图类型的 RecyclerView。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。