Android ListView 从入门到精通:基础用法 + 性能优化 + 实战避坑全指南,告别卡顿!

目录

前言

一、先搞懂:ListView 到底是什么?

1.1 ListView 的核心作用

1.2 核心工作流程(用生活场景理解)

1.3 ListView 与 Adapter 的绑定关系

二、新手入门:3 种基础用法快速上手

2.1 场景 1:纯文本列表(ArrayAdapter)

2.2 场景 2:图文混合列表(SimpleAdapter)

2.3 场景 3:自定义布局 + 交互(BaseAdapter)

三、进阶用法:解锁 ListView 更多实用功能

3.1 数据更新:正确刷新列表的 3 种方式

3.2 交互事件:点击、长按、多选

3.3 多类型布局:一个列表展示不同样式

四、性能优化:让 ListView 滑动如丝般顺滑

4.1 必做优化:ViewHolder 模式(已讲过,再强调)

4.2 关键优化:复用 convertView

4.3 进阶优化:图片加载优化

4.4 高级优化:分批加载数据(分页加载)

4.5 细节优化:减少布局层级 + 避免过度绘制

五、避坑指南:解决 ListView 开发中最常见的 8 个问题

5.1 问题 1:列表滑动时数据错乱(最常见)

5.2 问题 2:notifyDataSetChanged () 不生效

5.3 问题 3:列表项内部控件点击事件失效

5.4 问题 4:ListView 没有滑动到底部却触发了分页加载

5.5 问题 5:图片加载错位(滑动时图片乱跳)

5.6 问题 6:ListView 滑动卡顿,日志显示 “跳过多少帧”

5.7 问题 7:ListView 不显示分割线

5.8 问题 8:ListView 数据更新后,滚动位置重置到顶部

六、综合实战:完整的网络数据列表(含下拉刷新 + 分页)

6.1 案例需求

6.2 核心依赖

6.3 核心代码

七、总结与拓展

7.1 核心知识点回顾

7.2 ListView 与 RecyclerView 对比

7.3 拓展学习方向

7.4 实战建议


class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

前言

作为 Android 开发中最经典的列表控件,ListView 承载了无数 App 的核心展示功能 —— 从早期的短信列表、联系人页面,到如今的商品列表、新闻信息流,都能看到它的身影。虽然现在 RecyclerView 逐渐成为主流,但 ListView 依然是新手入门的必经之路,也是很多 legacy 项目的核心组件。

很多开发者对 ListView 的印象停留在 “简单但卡顿”,其实只要掌握了核心原理和优化技巧,它同样能实现丝滑流畅的列表体验。本文会从基础用法到进阶技巧,再到性能优化和避坑指南,用通俗易懂的语言 + 可直接运行的代码,带你彻底吃透 ListView,无论是新手入门还是老手重构旧项目,都能有所收获。

一、先搞懂:ListView 到底是什么?

新手刚接触时,很容易把 ListView 和 Adapter 搞混,其实一句话就能理清关系:ListView 是 “容器”,负责展示列表视图;Adapter 是 “桥梁”,负责把数据转换成视图并交给 ListView 展示

1.1 ListView 的核心作用

ListView 本质是一个 “可滚动的线性布局容器”,专门用于高效展示大量同类数据。它的核心优势是视图复用—— 不会为每个数据项都创建一个 View,而是只创建屏幕可见数量的 View,当列表滑动时,回收不可见的 View 并重新绑定新数据,从而节省内存、提升性能。

1.2 核心工作流程(用生活场景理解)

把 ListView 想象成手机屏幕上的 “展示窗口”,数据是 “待展示的商品”,Adapter 是 “理货员”,整个流程如下:

  1. 窗口(ListView)告诉理货员(Adapter):我能展示多少商品(屏幕可见数量)?
  2. 理货员(Adapter)从仓库(数据源)取出商品(数据),装进统一的包装盒(View);
  3. 理货员把包装好的商品放到窗口展示;
  4. 用户滑动窗口(滚动列表),不可见的商品被回收,理货员给回收的包装盒重新装上新商品,再放到窗口展示;
  5. 仓库商品更新(数据变化),理货员通知窗口重新展示(notifyDataSetChanged ())。

1.3 ListView 与 Adapter 的绑定关系

ListView 不能直接使用数据,必须通过 Adapter 中转,常见的 Adapter 有 3 种(和上一篇 Adapter 博客呼应,但侧重 ListView 场景):

  • ArrayAdapter:最简单的 Adapter,适合纯文本列表;
  • SimpleAdapter:支持图文混合列表,无需自定义 Adapter;
  • BaseAdapter:自定义程度最高,支持复杂布局和交互,是开发核心。

二、新手入门:3 种基础用法快速上手

这部分从最简单的场景开始,带大家快速感受 ListView 的使用逻辑,代码均提供 Java 和 Kotlin 双版本,兼顾不同开发者习惯。

2.1 场景 1:纯文本列表(ArrayAdapter)

适合展示单一文本类型的列表(如设置选项、菜单列表),一行代码即可实现,无需自定义布局。

核心步骤

  1. 布局文件中添加 ListView;
  2. 准备字符串数据源(数组或 List<String>);
  3. 创建 ArrayAdapter 并绑定 ListView;
  4. (可选)设置列表项点击事件。

完整代码

布局文件(activity_listview_text.xml)

<?xml version="1.0" encoding="utf-8"?>
<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/lv_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@color/gray_light"
        android:dividerHeight="1dp" />
</LinearLayout>

Activity 代码(Java 版)

public class TextListViewActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview_text);

        // 1. 找到 ListView 控件
        ListView lvText = findViewById(R.id.lv_text);

        // 2. 准备数据源(纯文本列表)
        String[] data = {"首页", "分类", "发现", "我的", "收藏", "历史", "设置", "帮助中心"};
        // 也可以用 List<String>:List<String> dataList = Arrays.asList(data);

        // 3. 创建 ArrayAdapter:参数(上下文,列表项布局,数据源)
        // android.R.layout.simple_list_item_1 是系统自带的纯文本布局
        ArrayAdapter<String> adapter = new ArrayAdapter<>(
                this,
                android.R.layout.simple_list_item_1,
                data
        );

        // 4. 给 ListView 设置 Adapter(绑定数据和视图)
        lvText.setAdapter(adapter);

        // 5. 列表项点击事件(可选)
        lvText.setOnItemClickListener((parent, view, position, id) -> {
            String selectedText = data[position];
            Toast.makeText(TextListViewActivity.this, "选中:" + selectedText, Toast.LENGTH_SHORT).show();
        });
    }
}

Activity 代码(Kotlin 版)

class TextListViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_listview_text)

        val lvText = findViewById<ListView>(R.id.lv_text)
        // 数据源
        val data = arrayOf("首页", "分类", "发现", "我的", "收藏", "历史", "设置", "帮助中心")
        // 创建 Adapter
        val adapter = ArrayAdapter(
            this,
            android.R.layout.simple_list_item_1,
            data
        )
        // 绑定 Adapter
        lvText.adapter = adapter
        // 点击事件
        lvText.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            val selectedText = data[position]
            Toast.makeText(this, "选中:$selectedText", Toast.LENGTH_SHORT).show()
        }
    }
}

关键说明

  • 系统提供了 3 种常用布局:
    • android.R.layout.simple_list_item_1:纯文本(常用);
    • android.R.layout.simple_list_item_2:双文本(主文本 + 副文本);
    • android.R.layout.simple_list_item_checked:带复选框的文本。
  • ArrayAdapter 仅支持文本展示,如需图文混合,需用 SimpleAdapter 或自定义 Adapter。

2.2 场景 2:图文混合列表(SimpleAdapter)

适合展示 “图片 + 文本” 的简单列表(如联系人、商品列表缩略图),无需自定义 Adapter,通过键值对映射数据和控件。

核心步骤

  1. 自定义列表项布局(包含 ImageView 和 TextView);
  2. 准备 List<Map<String, Object>> 类型数据源(键值对形式);
  3. 定义 “数据键” 和 “控件 ID” 的映射关系;
  4. 创建 SimpleAdapter 并绑定 ListView。

完整代码

列表项布局(item_simple_listview.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:gravity="center_vertical"
    android:paddingHorizontal="16dp"
    android:orientation="horizontal">

    <!-- 图片(头像/缩略图) -->
    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:scaleType="centerCrop"
        android:background="@drawable/shape_circle" />

    <!-- 文本容器 -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginStart="16dp"
        android:orientation="vertical">

        <!-- 主文本(如姓名/商品名) -->
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textStyle="bold" />

        <!-- 副文本(如描述/价格) -->
        <TextView
            android:id="@+id/tv_desc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:textSize="14sp"
            android:textColor="@color/gray" />
    </LinearLayout>
</LinearLayout>

Activity 代码(Kotlin 版)

class SimpleListViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_simple_listview)

        val lvSimple = findViewById<ListView>(R.id.lv_simple)

        // 1. 准备数据源:List<Map<String, Object>>,每个 Map 对应一个列表项
        val dataList = mutableListOf<Map<String, Any>>()
        // 列表项1
        dataList.add(
            mapOf(
                "icon" to R.drawable.ic_wechat, // 图片资源ID(键:icon)
                "title" to "微信", // 主文本(键:title)
                "desc" to "100+ 条未读消息" // 副文本(键:desc)
            )
        )
        // 列表项2
        dataList.add(
            mapOf(
                "icon" to R.drawable.ic_qq,
                "title" to "QQ",
                "desc" to "56 条未读消息"
            )
        )
        // 列表项3
        dataList.add(
            mapOf(
                "icon" to R.drawable.ic_alipay,
                "title" to "支付宝",
                "desc" to "余额:1234.56 元"
            )
        )

        // 2. 定义数据键(与 Map 中的键对应)
        val from = arrayOf("icon", "title", "desc")
        // 3. 定义控件ID(与 from 数组顺序一一对应)
        val to = intArrayOf(R.id.iv_icon, R.id.tv_title, R.id.tv_desc)

        // 4. 创建 SimpleAdapter
        val adapter = SimpleAdapter(
            this,
            dataList, // 数据源
            R.layout.item_simple_listview, // 自定义列表项布局
            from, // 数据键数组
            to // 控件ID数组
        )

        // 5. 绑定 Adapter
        lvSimple.adapter = adapter

        // 6. 点击事件
        lvSimple.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            val item = dataList[position]
            val title = item["title"] as String
            Toast.makeText(this, "打开:$title", Toast.LENGTH_SHORT).show()
        }
    }
}

Activity 布局(activity_simple_listview.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/lv_simple"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@color/gray_light"
        android:dividerHeight="1dp" />
</LinearLayout>

关键说明

  • SimpleAdapter 支持多控件映射,但仅适用于静态布局(无需动态修改控件样式);
  • ImageView 仅支持本地资源(drawable/mipmap),若需加载网络图片,需自定义 Adapter 配合 Glide/Picasso。

2.3 场景 3:自定义布局 + 交互(BaseAdapter)

实际开发中,列表项往往包含复杂布局(如按钮、进度条)或动态交互(如点赞、收藏),这时必须自定义 BaseAdapter—— 这是 ListView 开发的核心技能,也是新手的重点学习内容。

BaseAdapter 核心方法

自定义 BaseAdapter 需重写 4 个抽象方法,每个方法的作用如下:

方法名作用核心注意点
getCount()返回列表项数量直接返回数据源长度
getItem(position: Int)返回指定位置的数据源对象用于获取当前项数据
getItemId(position: Int)返回列表项 ID通常返回 position 即可
getView(position: Int, convertView: View?, parent: ViewGroup)创建 / 复用视图并绑定数据性能优化的核心的地方,需处理 convertView 复用和 ViewHolder 缓存

实战:带点赞功能的新闻列表

步骤 1:定义数据模型(NewsModel)

// 新闻数据模型:存储每条新闻的信息
data class NewsModel(
    val title: String, // 新闻标题
    val source: String, // 新闻来源(如“人民日报”)
    val time: String, // 发布时间
    val likeCount: Int, // 点赞数
    var isLiked: Boolean // 是否已点赞(可变状态)
)

步骤 2:列表项布局(item_news_listview.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical"
    android:background="?attr/selectableItemBackground">

    <!-- 新闻标题 -->
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"
        android:maxLines="2"
        android:ellipsize="end" />

    <!-- 来源和时间 -->
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_source"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:textColor="@color/gray" />

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:textSize="14sp"
            android:textColor="@color/gray" />
    </LinearLayout>

    <!-- 点赞区域 -->
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/iv_like"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:src="@drawable/ic_like_normal" />

        <TextView
            android:id="@+id/tv_like_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:textSize="14sp"
            android:textColor="@color/gray" />
    </LinearLayout>
</LinearLayout>

步骤 3:自定义 BaseAdapter(核心)

class NewsAdapter(
    private val context: Context,
    private val dataList: MutableList<NewsModel>
) : BaseAdapter() {

    // 1. 返回列表项数量(数据源长度)
    override fun getCount(): Int = dataList.size

    // 2. 返回指定位置的数据源对象
    override fun getItem(position: Int): NewsModel = dataList[position]

    // 3. 返回列表项ID(直接用position即可)
    override fun getItemId(position: Int): Long = position.toLong()

    // 4. 创建/复用视图并绑定数据(性能优化核心)
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        // ViewHolder:缓存控件实例,避免重复findViewById(关键优化)
        val holder: ViewHolder
        val view: View

        if (convertView == null) {
            // 第一次创建视图:加载布局、初始化控件、绑定ViewHolder
            view = LayoutInflater.from(context).inflate(R.layout.item_news_listview, parent, false)
            holder = ViewHolder()
            // 绑定控件(一次查找,多次复用)
            holder.tvTitle = view.findViewById(R.id.tv_title)
            holder.tvSource = view.findViewById(R.id.tv_source)
            holder.tvTime = view.findViewById(R.id.tv_time)
            holder.ivLike = view.findViewById(R.id.iv_like)
            holder.tvLikeCount = view.findViewById(R.id.tv_like_count)
            // 将ViewHolder存储到View的tag中,方便复用
            view.tag = holder
        } else {
            // 复用已有视图:直接从View的tag中获取ViewHolder
            view = convertView
            holder = view.tag as ViewHolder
        }

        // 绑定数据到控件(每次复用都要重新绑定,避免数据错乱)
        val news = getItem(position)
        holder.tvTitle?.text = news.title
        holder.tvSource?.text = news.source
        holder.tvTime?.text = news.time
        holder.tvLikeCount?.text = news.likeCount.toString()

        // 设置点赞状态(关键:每次绑定都要明确状态,避免复用导致的状态错乱)
        if (news.isLiked) {
            holder.ivLike?.setImageResource(R.drawable.ic_like_selected)
            holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.red))
        } else {
            holder.ivLike?.setImageResource(R.drawable.ic_like_normal)
            holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.gray))
        }

        // 点赞点击事件(动态修改状态并刷新)
        holder.ivLike?.setOnClickListener {
            // 修改数据源中的状态(状态必须存在数据源,不能存在View中)
            news.isLiked = !news.isLiked
            val newLikeCount = if (news.isLiked) news.likeCount + 1 else news.likeCount - 1
            news.likeCount = newLikeCount
            // 通知Adapter数据变化,更新当前项视图
            notifyDataSetChanged()
        }

        return view
    }

    // 静态内部类:ViewHolder,用于缓存控件
    private class ViewHolder {
        var tvTitle: TextView? = null
        var tvSource: TextView? = null
        var tvTime: TextView? = null
        var ivLike: ImageView? = null
        var tvLikeCount: TextView? = null
    }
}

步骤 4:Activity 中使用 Adapter

class NewsListViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_news_listview)

        val lvNews = findViewById<ListView>(R.id.lv_news)

        // 1. 初始化数据源
        val dataList = mutableListOf<NewsModel>().apply {
            add(NewsModel("Android ListView 性能优化实战", "技术干货君", "2小时前", 128, false))
            add(NewsModel("ListView 与 RecyclerView 核心差异分析", "Android 开发笔记", "5小时前", 89, true))
            add(NewsModel("新手必看:ListView 常见问题避坑指南", "编程小助手", "1天前", 215, false))
            add(NewsModel("ListView 自定义 Adapter 完整教程", "移动开发周刊", "2天前", 342, false))
            add(NewsModel("如何让 ListView 滑动流畅度提升50%", "架构师之路", "3天前", 567, true))
        }

        // 2. 创建自定义 Adapter
        val adapter = NewsAdapter(this, dataList)

        // 3. 绑定 Adapter 到 ListView
        lvNews.adapter = adapter

        // 4. 列表项点击事件(跳转到详情页)
        lvNews.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            val news = dataList[position]
            Toast.makeText(this, "查看详情:${news.title}", Toast.LENGTH_SHORT).show()
        }
    }
}

Activity 布局(activity_news_listview.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/lv_news"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@color/gray_light"
        android:dividerHeight="1dp"
        android:overScrollMode="never" />
</LinearLayout>

核心优化点:ViewHolder 模式

这是 ListView 性能优化的基石,必须理解透彻:

  • 没有 ViewHolder 时:每次调用 getView() 都会执行 findViewById(),而 findViewById() 是耗时操作(需要遍历视图树),滑动列表时会频繁调用,导致卡顿;
  • 有 ViewHolder 时:控件实例被缓存到 ViewHolder 中,convertView 复用时直接从 View.tag 中取出,无需重复查找,滑动流畅度大幅提升。

三、进阶用法:解锁 ListView 更多实用功能

基础用法只能满足简单需求,实际开发中还会遇到 “数据更新”“长按事件”“多类型布局” 等场景,这部分带你解锁 ListView 的进阶技能。

3.1 数据更新:正确刷新列表的 3 种方式

列表数据变化后(如新增、删除、修改),需要通知 Adapter 刷新视图,常见的 3 种方式如下:

方式 1:全局刷新(notifyDataSetChanged ())

最常用的方式,会刷新整个列表的所有视图,适合数据变化较多的场景:

// 新增数据
dataList.add(NewsModel("新增新闻标题", "测试来源", "刚刚", 0, false))
// 通知 Adapter 刷新
adapter.notifyDataSetChanged()

方式 2:局部刷新(notifyItemChanged () 等)

API 11+ 提供的局部刷新方法,只刷新指定位置的视图,性能更优:

// 刷新单个位置(第2项)
adapter.notifyItemChanged(1)
// 刷新指定范围(从第0项开始,刷新3项)
adapter.notifyItemRangeChanged(0, 3)
// 新增数据后刷新新增位置
val newPosition = dataList.size
dataList.add(newItem)
adapter.notifyItemInserted(newPosition)
// 删除数据后刷新删除位置
dataList.removeAt(2)
adapter.notifyItemRemoved(2)

方式 3:替换数据源刷新

适合完全替换列表数据的场景(如下拉刷新):

// 替换整个数据源
dataList.clear()
dataList.addAll(newDataList)
// 全局刷新
adapter.notifyDataSetChanged()

关键注意事项

  • 数据更新必须在主线程执行,否则会报错;
  • 局部刷新方法(如 notifyItemChanged())需要 Adapter 继承 BaseAdapter 或 ArrayAdapter(需 API 11+);
  • 刷新前必须确保数据源已修改,否则刷新无效。

3.2 交互事件:点击、长按、多选

ListView 提供了丰富的交互事件回调,满足日常开发需求:

(1)列表项点击事件(OnItemClickListener)

基础用法中已演示,用于响应列表项的点击操作:

lvNews.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
    val news = dataList[position]
    // 跳转到详情页
    val intent = Intent(this, NewsDetailActivity::class.java)
    intent.putExtra("news_title", news.title)
    startActivity(intent)
}

(2)列表项长按事件(OnItemLongClickListener)

用于响应长按操作(如弹出菜单、删除 item):

lvNews.onItemLongClickListener = AdapterView.OnItemLongClickListener { parent, view, position, id ->
    // 弹出删除菜单
    AlertDialog.Builder(this)
        .setTitle("提示")
        .setMessage("确定要删除这条新闻吗?")
        .setPositiveButton("删除") { _, _ ->
            // 删除数据源中的数据
            dataList.removeAt(position)
            // 刷新列表
            adapter.notifyDataSetChanged()
        }
        .setNegativeButton("取消", null)
        .show()
    // 返回true表示消费事件,避免同时触发点击事件
    true
}

(3)多选功能(ChoiceMode)

通过设置 choiceMode 实现多选或单选功能,适合批量操作(如批量删除):

// 1. 在布局中设置 choiceMode
<ListView
    android:id="@+id/lv_choice"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:choiceMode="multipleChoice" />

// 2. 代码中获取选中的项
val selectedPositions = mutableListOf<Int>()
// 遍历所有项,判断是否选中
for (i in 0 until adapter.count) {
    if (lvChoice.isItemChecked(i)) {
        selectedPositions.add(i)
    }
}
// 批量删除选中项(注意:倒序删除,避免索引错乱)
for (i in selectedPositions.reversed()) {
    dataList.removeAt(i)
}
adapter.notifyDataSetChanged()

(4)列表项内部控件点击事件

如列表项中的按钮、图片,需要在 getView() 中给控件设置点击事件(如之前点赞功能的实现):

// 在 getView() 中给按钮设置点击事件
holder.btnDelete?.setOnClickListener {
    val news = getItem(position)
    Toast.makeText(context, "删除:${news.title}", Toast.LENGTH_SHORT).show()
    dataList.removeAt(position)
    notifyDataSetChanged()
}

3.3 多类型布局:一个列表展示不同样式

实际开发中,可能需要在一个列表中展示多种布局(如首页的 Banner 轮播 + 新闻列表 + 推荐卡片),这时需要通过 getItemViewType() 和 getViewTypeCount() 实现。

实战:包含 Banner 和新闻的混合列表

步骤 1:定义多类型数据模型(密封类)

// 密封类:统一管理多类型数据
sealed class MixedItem {
    // Banner 类型:包含轮播图图片地址
    data class BannerItem(val imageUrls: List<String>) : MixedItem()
    // 新闻类型:复用之前的 NewsModel
    data class NewsItem(val news: NewsModel) : MixedItem()
}

步骤 2:定义两种布局

  • Banner 布局(item_banner.xml):
<ImageView
    android:id="@+id/iv_banner"
    android:layout_width="match_parent"
    android:layout_height="180dp"
    android:scaleType="centerCrop"
    android:background="@color/gray_light" />
  • 新闻布局:复用之前的 item_news_listview.xml

步骤 3:自定义多类型 BaseAdapter

class MixedAdapter(
    private val context: Context,
    private val dataList: MutableList<MixedItem>
) : BaseAdapter() {

    // 定义布局类型常量
    companion object {
        private const val TYPE_BANNER = 0 // Banner 类型
        private const val TYPE_NEWS = 1 // 新闻类型
    }

    // 1. 返回布局类型数量
    override fun getViewTypeCount(): Int = 2

    // 2. 返回当前位置的布局类型
    override fun getItemViewType(position: Int): Int {
        return when (dataList[position]) {
            is MixedItem.BannerItem -> TYPE_BANNER
            is MixedItem.NewsItem -> TYPE_NEWS
        }
    }

    // 3. 返回列表项数量
    override fun getCount(): Int = dataList.size

    // 4. 返回当前项数据
    override fun getItem(position: Int): MixedItem = dataList[position]

    // 5. 返回列表项ID
    override fun getItemId(position: Int): Long = position.toLong()

    // 6. 创建/复用视图(根据布局类型处理)
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val viewType = getItemViewType(position)
        var view: View? = convertView
        var bannerHolder: BannerViewHolder? = null
        var newsHolder: NewsViewHolder? = null

        when (viewType) {
            TYPE_BANNER -> {
                if (view == null) {
                    // 加载 Banner 布局
                    view = LayoutInflater.from(context).inflate(R.layout.item_banner, parent, false)
                    bannerHolder = BannerViewHolder()
                    bannerHolder.ivBanner = view.findViewById(R.id.iv_banner)
                    view.tag = bannerHolder
                } else {
                    bannerHolder = view.tag as BannerViewHolder
                }

                // 绑定 Banner 数据
                val bannerItem = getItem(position) as MixedItem.BannerItem
                // 用 Glide 加载第一张轮播图(实际开发中需用轮播图控件)
                Glide.with(context)
                    .load(bannerItem.imageUrls[0])
                    .placeholder(R.color.gray_light)
                    .into(bannerHolder.ivBanner!!)
            }

            TYPE_NEWS -> {
                if (view == null) {
                    // 加载新闻布局
                    view = LayoutInflater.from(context).inflate(R.layout.item_news_listview, parent, false)
                    newsHolder = NewsViewHolder()
                    // 绑定新闻布局控件
                    newsHolder.tvTitle = view.findViewById(R.id.tv_title)
                    newsHolder.tvSource = view.findViewById(R.id.tv_source)
                    newsHolder.tvTime = view.findViewById(R.id.tv_time)
                    newsHolder.ivLike = view.findViewById(R.id.iv_like)
                    newsHolder.tvLikeCount = view.findViewById(R.id.tv_like_count)
                    view.tag = newsHolder
                } else {
                    newsHolder = view.tag as NewsViewHolder
                }

                // 绑定新闻数据
                val newsItem = getItem(position) as MixedItem.NewsItem
                val news = newsItem.news
                newsHolder.tvTitle?.text = news.title
                newsHolder.tvSource?.text = news.source
                newsHolder.tvTime?.text = news.time
                newsHolder.tvLikeCount?.text = news.likeCount.toString()

                // 点赞状态
                if (news.isLiked) {
                    newsHolder.ivLike?.setImageResource(R.drawable.ic_like_selected)
                    newsHolder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.red))
                } else {
                    newsHolder.ivLike?.setImageResource(R.drawable.ic_like_normal)
                    newsHolder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.gray))
                }
            }
        }

        return view!!
    }

    // Banner 布局的 ViewHolder
    private class BannerViewHolder {
        var ivBanner: ImageView? = null
    }

    // 新闻布局的 ViewHolder
    private class NewsViewHolder {
        var tvTitle: TextView? = null
        var tvSource: TextView? = null
        var tvTime: TextView? = null
        var ivLike: ImageView? = null
        var tvLikeCount: TextView? = null
    }
}

步骤 4:Activity 中使用

class MixedListViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mixed_listview)

        val lvMixed = findViewById<ListView>(R.id.lv_mixed)

        // 初始化多类型数据源
        val dataList = mutableListOf<MixedItem>().apply {
            // 添加 Banner 项
            add(MixedItem.BannerItem(
                listOf(
                    "https://picsum.photos/800/400?1",
                    "https://picsum.photos/800/400?2",
                    "https://picsum.photos/800/400?3"
                )
            ))
            // 添加新闻项
            add(MixedItem.NewsItem(NewsModel("ListView 多类型布局实战", "技术君", "1小时前", 66, false)))
            add(MixedItem.NewsItem(NewsModel("Android 混合列表开发技巧", "开发笔记", "3小时前", 99, true)))
        }

        // 创建 Adapter 并绑定
        val adapter = MixedAdapter(this, dataList)
        lvMixed.adapter = adapter
    }
}

四、性能优化:让 ListView 滑动如丝般顺滑

很多开发者抱怨 ListView 卡顿,其实大部分卡顿都是因为没有做好优化。掌握以下 5 个核心优化技巧,能让 ListView 的滑动流畅度提升 50% 以上。

4.1 必做优化:ViewHolder 模式(已讲过,再强调)

这是最基础也是最重要的优化,核心是缓存控件实例,避免重复 findViewById ()

  • 没有 ViewHolder 时:滑动列表会频繁执行 findViewById(),遍历视图树耗时,导致卡顿;
  • 有 ViewHolder 时:控件实例只查找一次,复用视图时直接取出,性能大幅提升。

4.2 关键优化:复用 convertView

getView() 方法的 convertView 参数是系统回收的不可见视图,必须复用,不能每次都 inflate 新布局:

错误写法(严禁)

// 每次都加载新布局,不复用 convertView,内存暴涨+卡顿
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    val view = LayoutInflater.from(context).inflate(R.layout.item_news, parent, false)
    // ... 绑定数据
    return view
}

正确写法(必须)

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    val view: View
    if (convertView == null) {
        view = LayoutInflater.from(context).inflate(R.layout.item_news, parent, false)
        // ... 初始化 ViewHolder
    } else {
        view = convertView
        // ... 取出 ViewHolder
    }
    // ... 绑定数据
    return view
}

4.3 进阶优化:图片加载优化

列表中的图片是性能消耗的重灾区,优化不当会导致 OOM(内存溢出)或滑动卡顿,核心优化点如下:

(1)使用图片加载库(Glide/Picasso)

自带图片压缩、缓存、延迟加载功能,避免手动处理图片导致的问题:

Glide.with(context)
    .load(imageUrl) // 图片地址(网络/本地)
    .placeholder(R.drawable.ic_placeholder) // 占位图
    .error(R.drawable.ic_error) // 加载失败图
    .override(300, 200) // 压缩图片尺寸(适应列表项大小)
    .centerCrop() // 缩放模式
    .diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存策略(内存+磁盘)
    .into(imageView)

(2)避免加载过大图片

根据列表项的实际尺寸加载对应分辨率的图片,比如列表项图片尺寸是 300x200,就不要加载 1080x1920 的原图。

(3)列表滑动时暂停图片加载

监听 ListView 的滑动状态,滑动时暂停加载,停止滑动后再加载,减少滑动时的性能消耗:

lvNews.setOnScrollListener(object : AbsListView.OnScrollListener {
    override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
        when (scrollState) {
            // 滑动时暂停加载
            AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL -> {
                Glide.with(context).pauseRequests()
            }
            // 停止滑动后恢复加载
            AbsListView.OnScrollListener.SCROLL_STATE_IDLE -> {
                Glide.with(context).resumeRequests()
            }
        }
    }

    override fun onScroll(view: AbsListView?, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {}
})

4.4 高级优化:分批加载数据(分页加载)

当列表数据量较大(如几百条、几千条)时,一次性加载所有数据会导致初始化缓慢、内存占用过高。分批加载(分页)是解决这个问题的关键:

核心逻辑

  1. 首次加载前 N 条数据(如 20 条);
  2. 监听 ListView 滑动到底部,加载下一页数据;
  3. 用 notifyItemRangeInserted() 刷新新增数据(避免全局刷新)。

实战代码

class PagingAdapter(
    private val context: Context,
    private val dataList: MutableList<NewsModel>
) : BaseAdapter() {
    // 标记是否正在加载(避免重复请求)
    var isLoading = false

    // 添加分页数据
    fun addPagingData(newData: List<NewsModel>) {
        val startPosition = dataList.size
        dataList.addAll(newData)
        // 只刷新新增的项,性能更优
        notifyItemRangeInserted(startPosition, newData.size)
        isLoading = false
    }

    // ... 其他方法(getCount、getItem、getView 等,和之前一致)
}

// Activity 中实现分页逻辑
class PagingListViewActivity : AppCompatActivity() {
    private lateinit var lvPaging: ListView
    private lateinit var adapter: PagingAdapter
    private val dataList = mutableListOf<NewsModel>()
    private var currentPage = 1 // 当前页码
    private val pageSize = 20 // 每页数据量

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging_listview)

        lvPaging = findViewById(R.id.lv_paging)
        adapter = PagingAdapter(this, dataList)
        lvPaging.adapter = adapter

        // 首次加载第一页数据
        loadData(currentPage)

        // 监听滑动到底部,加载下一页
        lvPaging.setOnScrollListener(object : AbsListView.OnScrollListener {
            override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {}

            override fun onScroll(
                view: AbsListView?,
                firstVisibleItem: Int,
                visibleItemCount: Int,
                totalItemCount: Int
            ) {
                // 滑动到底部且不在加载中,加载下一页
                if (!adapter.isLoading 
                    && firstVisibleItem + visibleItemCount >= totalItemCount - 5) {
                    currentPage++
                    loadData(currentPage)
                }
            }
        })
    }

    // 模拟网络请求加载数据
    private fun loadData(page: Int) {
        adapter.isLoading = true
        // 显示加载中提示(可在列表底部添加加载视图)
        showLoadingFooter(true)

        // 协程模拟网络请求(实际开发中替换为真实接口请求)
        lifecycleScope.launch {
            delay(1000) // 模拟网络延迟

            // 模拟分页数据
            val newData = mutableListOf<NewsModel>()
            for (i in 0 until pageSize) {
                newData.add(
                    NewsModel(
                        title = "第 $page 页 - 新闻标题 ${i+1}",
                        source = "分页加载测试",
                        time = "${page}小时前",
                        likeCount = Random.nextInt(0, 500),
                        isLiked = false
                    )
                )
            }

            // 添加数据到 Adapter
            adapter.addPagingData(newData)
            // 隐藏加载中提示
            showLoadingFooter(false)
        }
    }

    // 显示/隐藏加载中底部视图(简化实现)
    private fun showLoadingFooter(show: Boolean) {
        if (show) {
            Toast.makeText(this, "加载中...", Toast.LENGTH_SHORT).show()
        } else {
            // 实际开发中可移除底部加载视图
        }
    }
}

4.5 细节优化:减少布局层级 + 避免过度绘制

  1. 减少布局层级:用 ConstraintLayout 替代多层 LinearLayout,避免嵌套过深(如列表项布局用 ConstraintLayout 替代 LinearLayout 嵌套);
  2. 移除不必要的背景:如果父布局和子布局的背景重叠,移除子布局的背景,避免过度绘制;
  3. 使用 merge 标签:列表项布局的根标签用 merge,复用父布局的布局参数,减少一层布局层级:
<!-- 优化前:LinearLayout 作为根布局,增加层级 -->
<LinearLayout ...>
    <!-- 控件... -->
</LinearLayout>

<!-- 优化后:merge 标签,减少层级 -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 控件... -->
</merge>
  1. 避免在 getView () 中执行耗时操作:如网络请求、数据库查询、复杂计算,这些操作应在子线程执行,执行完成后再更新 UI。

五、避坑指南:解决 ListView 开发中最常见的 8 个问题

ListView 开发中,很多新手会遇到各种 “玄学问题”,其实都是有固定原因的。下面总结 8 个高频问题,每个问题都讲清 “原因 + 解决方案”,让你少踩坑。

5.1 问题 1:列表滑动时数据错乱(最常见)

原因

视图复用导致的状态混乱 —— 比如点赞状态、选中状态只存储在 View 中,复用视图时未重新设置状态,导致显示上一个 item 的状态。

解决方案

  • 状态必须存储在数据源中(如 NewsModel 中的 isLiked 字段),不能存储在 View 或 ViewHolder 中;
  • 每次调用 getView() 时,无论视图是否复用,都要明确设置控件状态(如点赞图标、文本颜色)。

错误示例(严禁)

// 状态存储在 ViewHolder 中,复用后会错乱
private class ViewHolder {
    var ivLike: ImageView? = null
    var isLiked: Boolean = false // 错误:状态存储在 ViewHolder
}

正确示例(必须)

// 状态存储在数据源中
data class NewsModel(..., var isLiked: Boolean)

// getView() 中明确设置状态
if (news.isLiked) {
    holder.ivLike?.setImageResource(R.drawable.ic_like_selected)
} else {
    holder.ivLike?.setImageResource(R.drawable.ic_like_normal)
}

5.2 问题 2:notifyDataSetChanged () 不生效

原因

  • 数据源未真正修改(如用 val 定义的列表,重新赋值后未通知 Adapter);
  • Adapter 引用的数据源对象未更新(如修改了列表元素但未调用 notify);
  • 在子线程中调用 notifyDataSetChanged()

解决方案

  • 可变集合MutableList)存储数据,修改后调用 notify 方法;
  • 如果替换整个数据源,确保 Adapter 引用的是新的列表对象,再调用 notify
  • 必须在主线程调用 notify 方法。

正确示例

// 1. 用 MutableList 存储数据
private val dataList = mutableListOf<NewsModel>()

// 2. 修改数据后调用 notify
dataList.add(newItem)
adapter.notifyDataSetChanged()

// 3. 子线程中更新数据(需切换到主线程)
lifecycleScope.launch(Dispatchers.IO) {
    // 子线程获取数据
    val newData = fetchData()
    // 切换到主线程更新 UI
    withContext(Dispatchers.Main) {
        dataList.addAll(newData)
        adapter.notifyDataSetChanged()
    }
}

5.3 问题 3:列表项内部控件点击事件失效

原因

  • 控件设置了 android:clickable="false"
  • 列表项的根布局设置了 onClick 事件,与内部控件点击事件冲突;
  • 控件被其他视图遮挡。

解决方案

  • 确保内部控件的 clickable 属性为 true(默认是 true,无需手动设置);
  • 内部控件点击事件优先级高于列表项点击事件,无需担心冲突;
  • 检查布局是否有遮挡(如父布局的 padding 或 margin 设置不当)。

5.4 问题 4:ListView 没有滑动到底部却触发了分页加载

原因

onScroll 方法中判断滑动到底部的条件不准确,导致提前触发加载。

解决方案

使用精准的判断条件:

override fun onScroll(
    view: AbsListView?,
    firstVisibleItem: Int,
    visibleItemCount: Int,
    totalItemCount: Int
) {
    // 条件:不在加载中 + 可见项数量 > 0 + 最后一个可见项位置 >= 总数量 - 5(预加载)
    val isBottom = firstVisibleItem + visibleItemCount >= totalItemCount - 5
    if (!adapter.isLoading && visibleItemCount > 0 && isBottom) {
        // 加载下一页
    }
}

5.5 问题 5:图片加载错位(滑动时图片乱跳)

原因

图片加载是异步操作,视图复用时,上一个图片的加载请求还未完成,导致新图片覆盖旧图片,或旧图片显示在新视图上。

解决方案

  • 使用图片加载库的 clear() 方法,加载新图片前清除旧图片;
  • 给 ImageView 设置 tag,确保图片加载完成后绑定到正确的视图上。

代码示例

// 给 ImageView 设置 tag(图片地址)
imageView.tag = imageUrl
// 加载图片前清除旧图片
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.ic_placeholder)
    .error(R.drawable.ic_error)
    .centerCrop()
    .into(object : CustomTarget<Drawable>() {
        override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
            // 验证 tag 是否匹配,避免错位
            if (imageView.tag == imageUrl) {
                imageView.setImageDrawable(resource)
            }
        }

        override fun onLoadCleared(placeholder: Drawable?) {
            imageView.setImageDrawable(placeholder)
        }
    })

5.6 问题 6:ListView 滑动卡顿,日志显示 “跳过多少帧”

原因

  • 没有使用 ViewHolder 模式,频繁 findViewById()
  • 图片加载未优化,尺寸过大或未缓存;
  • getView() 中执行了耗时操作(如复杂计算、IO 操作);
  • 布局层级过深,过度绘制严重。

解决方案

  • 严格使用 ViewHolder 模式;
  • 按前面的图片优化技巧处理图片;
  • 耗时操作移到子线程执行;
  • 优化布局层级,减少过度绘制(可通过 “开发者选项→调试 GPU 过度绘制” 查看)。

5.7 问题 7:ListView 不显示分割线

原因

  • 未设置 divider 或 dividerHeight
  • divider 设置为透明色;
  • 列表项布局的背景覆盖了分割线。

解决方案

  • 在布局中明确设置分割线和高度:
<ListView
    android:id="@+id/lv_news"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@color/gray_light" // 分割线颜色
    android:dividerHeight="1dp" /> // 分割线高度
  • 确保列表项布局的背景不会覆盖分割线(如避免设置 layout_marginBottom 为负数)。

5.8 问题 8:ListView 数据更新后,滚动位置重置到顶部

原因

调用 notifyDataSetChanged() 后,ListView 会重新计算布局,导致滚动位置重置。

解决方案

更新数据前保存滚动位置,更新后恢复:

// 保存滚动位置
val firstVisiblePosition = lvNews.firstVisiblePosition
val top = (lvNews.getChildAt(0)?.top ?: 0) - lvNews.paddingTop

// 更新数据
dataList.clear()
dataList.addAll(newData)
adapter.notifyDataSetChanged()

// 恢复滚动位置
lvNews.setSelectionFromTop(firstVisiblePosition, top)

六、综合实战:完整的网络数据列表(含下拉刷新 + 分页)

整合前面的知识点,实现一个 “网络请求 + 下拉刷新 + 上拉分页 + 点赞功能 + 局部刷新” 的完整列表,模拟真实开发场景。

6.1 案例需求

  1. 调用公开 API 获取新闻列表数据;
  2. 支持下拉刷新(刷新第一页数据);
  3. 支持上拉加载更多(分页加载);
  4. 列表项包含标题、来源、时间、点赞功能;
  5. 点赞后局部刷新点赞状态和数量,不刷新整个列表。

6.2 核心依赖

// 网络请求
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 图片加载
implementation 'com.github.bumptech.glide:glide:4.16.0'
kapt 'com.github.bumptech.glide:compiler:4.16.0'
// 下拉刷新
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

6.3 核心代码

步骤 1:网络请求相关(API 接口、数据模型)

// 新闻数据模型(适配 API 返回格式)
data class NewsResponse(
    val code: Int,
    val msg: String,
    val data: List<NewsModel>
)

// 新闻实体类(复用之前的 NewsModel)
data class NewsModel(
    val id: Long,
    val title: String,
    val source: String,
    val time: String,
    val likeCount: Int,
    var isLiked: Boolean
)

// Retrofit API 接口
interface NewsApiService {
    @GET("news/list")
    suspend fun getNewsList(
        @Query("page") page: Int,
        @Query("size") size: Int
    ): Response<NewsResponse>
}

// 网络请求工具类
object RetrofitClient {
    private const val BASE_URL = "https://api.example.com/" // 替换为真实 API 地址

    val apiService: NewsApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(NewsApiService::class.java)
    }
}

步骤 2:自定义 Adapter(支持局部刷新)

class CompleteNewsAdapter(
    private val context: Context,
    private val dataList: MutableList<NewsModel>,
    private val onLikeClick: (Long, Boolean) -> Unit // 点赞回调(新闻ID,是否点赞)
) : BaseAdapter() {

    // ViewHolder 缓存控件
    private class ViewHolder {
        var tvTitle: TextView? = null
        var tvSource: TextView? = null
        var tvTime: TextView? = null
        var ivLike: ImageView? = null
        var tvLikeCount: TextView? = null
    }

    override fun getCount(): Int = dataList.size

    override fun getItem(position: Int): NewsModel = dataList[position]

    override fun getItemId(position: Int): Long = dataList[position].id

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val holder: ViewHolder
        val view: View

        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(R.layout.item_news_listview, parent, false)
            holder = ViewHolder()
            holder.tvTitle = view.findViewById(R.id.tv_title)
            holder.tvSource = view.findViewById(R.id.tv_source)
            holder.tvTime = view.findViewById(R.id.tv_time)
            holder.ivLike = view.findViewById(R.id.iv_like)
            holder.tvLikeCount = view.findViewById(R.id.tv_like_count)
            view.tag = holder
        } else {
            view = convertView
            holder = view.tag as ViewHolder
        }

        // 绑定数据
        val news = getItem(position)
        holder.tvTitle?.text = news.title
        holder.tvSource?.text = news.source
        holder.tvTime?.text = news.time
        holder.tvLikeCount?.text = news.likeCount.toString()

        // 点赞状态
        if (news.isLiked) {
            holder.ivLike?.setImageResource(R.drawable.ic_like_selected)
            holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.red))
        } else {
            holder.ivLike?.setImageResource(R.drawable.ic_like_normal)
            holder.tvLikeCount?.setTextColor(ContextCompat.getColor(context, R.color.gray))
        }

        // 点赞点击事件
        holder.ivLike?.setOnClickListener {
            val newLiked = !news.isLiked
            val newLikeCount = if (newLiked) news.likeCount + 1 else news.likeCount - 1
            // 更新数据源
            news.isLiked = newLiked
            news.likeCount = newLikeCount
            // 局部刷新当前项(只刷新点赞相关控件,性能最优)
            notifyItemChanged(position)
            // 回调给 Activity 发送点赞网络请求
            onLikeClick(news.id, newLiked)
        }

        return view
    }

    // 添加分页数据
    fun addPagingData(newData: List<NewsModel>) {
        val startPos = dataList.size
        dataList.addAll(newData)
        notifyItemRangeInserted(startPos, newData.size)
    }

    // 刷新第一页数据(下拉刷新)
    fun refreshData(newData: List<NewsModel>) {
        dataList.clear()
        dataList.addAll(newData)
        notifyDataSetChanged()
    }
}

步骤 3:Activity 实现(下拉刷新 + 分页加载)

class CompleteListViewActivity : AppCompatActivity() {
    private lateinit var swipeRefreshLayout: SwipeRefreshLayout
    private lateinit var lvNews: ListView
    private lateinit var adapter: CompleteNewsAdapter
    private val dataList = mutableListOf<NewsModel>()
    private var currentPage = 1
    private val pageSize = 20
    private var isLoading = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_complete_listview)

        // 初始化控件
        swipeRefreshLayout = findViewById(R.id.srl_refresh)
        lvNews = findViewById(R.id.lv_news)

        // 初始化 Adapter
        adapter = CompleteNewsAdapter(this, dataList) { newsId, isLiked ->
            // 点赞回调:发送网络请求(实际开发中实现)
            Toast.makeText(this, "点赞状态:$isLiked", Toast.LENGTH_SHORT).show()
        }
        lvNews.adapter = adapter

        // 下拉刷新配置
        swipeRefreshLayout.setColorSchemeResources(R.color.blue)
        swipeRefreshLayout.setOnRefreshListener {
            // 刷新第一页
            currentPage = 1
            loadNewsData(currentPage, isRefresh = true)
        }

        // 上拉加载更多
        lvNews.setOnScrollListener(object : AbsListView.OnScrollListener {
            override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {}

            override fun onScroll(
                view: AbsListView?,
                firstVisibleItem: Int,
                visibleItemCount: Int,
                totalItemCount: Int
            ) {
                val isBottom = firstVisibleItem + visibleItemCount >= totalItemCount - 5
                if (!isLoading && !swipeRefreshLayout.isRefreshing && visibleItemCount > 0 && isBottom) {
                    currentPage++
                    loadNewsData(currentPage, isRefresh = false)
                }
            }
        })

        // 首次加载数据
        loadNewsData(currentPage, isRefresh = false)
    }

    // 加载新闻数据
    private fun loadNewsData(page: Int, isRefresh: Boolean) {
        isLoading = true
        if (!isRefresh) {
            Toast.makeText(this, "加载中...", Toast.LENGTH_SHORT).show()
        }

        // 协程发起网络请求
        lifecycleScope.launch(Dispatchers.IO) {
            try {
                val response = RetrofitClient.apiService.getNewsList(page, pageSize)
                if (response.isSuccessful) {
                    val newsResponse = response.body()
                    if (newsResponse?.code == 200) {
                        val newData = newsResponse.data ?: emptyList()
                        // 切换到主线程更新 UI
                        withContext(Dispatchers.Main) {
                            if (isRefresh) {
                                // 下拉刷新:替换数据
                                adapter.refreshData(newData)
                                swipeRefreshLayout.isRefreshing = false
                            } else {
                                // 上拉加载:追加数据
                                adapter.addPagingData(newData)
                            }
                        }
                    } else {
                        withContext(Dispatchers.Main) {
                            Toast.makeText(this@CompleteListViewActivity, "加载失败:${newsResponse?.msg}", Toast.LENGTH_SHORT).show()
                            swipeRefreshLayout.isRefreshing = false
                        }
                    }
                } else {
                    withContext(Dispatchers.Main) {
                        Toast.makeText(this@CompleteListViewActivity, "请求失败", Toast.LENGTH_SHORT).show()
                        swipeRefreshLayout.isRefreshing = false
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    Toast.makeText(this@CompleteListViewActivity, "网络错误", Toast.LENGTH_SHORT).show()
                    swipeRefreshLayout.isRefreshing = false
                }
            } finally {
                isLoading = false
            }
        }
    }
}

步骤 4:Activity 布局(含下拉刷新)

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/srl_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/lv_news"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@color/gray_light"
        android:dividerHeight="1dp"
        android:overScrollMode="never" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

七、总结与拓展

7.1 核心知识点回顾

  1. ListView 是 Android 经典列表控件,核心优势是视图复用
  2. 基础用法:ArrayAdapter(纯文本)、SimpleAdapter(简单图文)、BaseAdapter(自定义);
  3. 进阶用法:多类型布局、数据更新、交互事件(点击 / 长按 / 多选);
  4. 性能优化:ViewHolder 模式、convertView 复用、图片优化、分页加载、布局优化;
  5. 避坑重点:数据错乱(状态存数据源)、刷新不生效(主线程 + 可变集合)、图片错位(tag + 清除旧请求)。

7.2 ListView 与 RecyclerView 对比

很多开发者会纠结选 ListView 还是 RecyclerView,这里做一个简洁对比,帮你快速选择:

特性ListViewRecyclerView
视图复用支持(需手动处理 convertView)支持(强制 ViewHolder)
布局支持仅线性布局(通过 ListAdapter)线性、网格、瀑布流(LayoutManager)
动画支持基本不支持自带 Item 动画,支持自定义
多类型布局支持(需重写 getItemViewType)支持(更简洁)
性能一般(需手动优化)更优(分级缓存 + 强制优化)
扩展性较差极强(模块化设计)
适用场景简单列表、legacy 项目复杂列表、新项目、性能要求高

7.3 拓展学习方向

  1. 自定义 ListView 分割线(ItemDecoration 类似功能);
  2. ListView 下拉刷新的自定义样式;
  3. 列表项侧滑删除功能(需自定义触摸事件);
  4. 结合 ViewModel、LiveData 实现数据与 UI 分离;
  5. 学习 RecyclerView 的高级用法(作为 ListView 的升级替代)。

7.4 实战建议

  1. 新手从 BaseAdapter 入手,理解视图复用和 ViewHolder 原理,这是列表开发的基础;
  2. 实际开发中,简单列表可用 ListView,复杂列表(如多类型、瀑布流)优先用 RecyclerView;
  3. 无论用哪种控件,性能优化的核心都是 “减少不必要的创建和查找”,比如缓存控件、复用视图、分批加载;
  4. 多动手写代码,尤其是性能优化和避坑部分,只有实战才能真正掌握。

这篇文章从基础到进阶,覆盖了 ListView 的全部核心知识点,所有代码均为实战原创,可直接复制运行。如果你正在开发或维护使用 ListView 的项目,这篇文章能帮你解决大部分问题。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值