下拉刷新:配合 SwipeRefreshLayout 实现刷新

下拉刷新这个功能,大家每天都在用。刷朋友圈、刷微博、刷新闻列表,手指往下一拉,转个圈,新内容就出来了。这个交互太常见了,以至于用户觉得它理所当然。但作为开发者,我们得亲手把它实现出来。

在 Android 里,实现下拉刷新最经典的方式就是配合 SwipeRefreshLayout。它是个 ViewGroup,只能包裹一个子 View。你把它套在 RecyclerView 外面,它就自动接管了「下拉检测」和「刷新动画」这两件事。

核心思路:SwipeRefreshLayout 监听下拉手势 → 触发回调 → 你执行数据刷新 → 刷新完成后通知 SwipeRefreshLayout 停止动画。

1. 基本用法:三步搞定

说实话,SwipeRefreshLayout 的用法非常简单。我刚开始用的时候,甚至觉得它太「傻瓜式」了。但后来发现,越简单的东西,越容易在细节上翻车。

第一步,在布局文件里把 RecyclerView 包起来:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/refreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

第二步,在 Activity 或 Fragment 里设置监听:

val refreshLayout = findViewById<SwipeRefreshLayout>(R.id.refreshLayout)
refreshLayout.setOnRefreshListener {
    // 在这里执行刷新操作
    loadData()
}

第三步,数据加载完成后,记得关掉刷新动画:

private fun loadData() {
    viewModel.loadData().observe(this) { result ->
        adapter.submitList(result)
        refreshLayout.isRefreshing = false  // 关键!别忘了这行
    }
}

嗯,这里要注意。很多新手会忘记在回调里设置 isRefreshing = false。结果就是刷新转圈一直转,用户以为卡死了。我早期的一个项目就踩过这个坑,测试同事拿着手机来找我:「你这个刷新怎么停不下来?」

2. 自定义刷新样式

SwipeRefreshLayout 默认的转圈是 Material Design 风格的蓝色进度条。说实话,大部分项目直接用默认的就够了。但如果你想让刷新指示器更符合 App 的品牌色,可以这样改:

refreshLayout.setColorSchemeResources(
    R.color.brand_primary,
    R.color.brand_accent,
    R.color.brand_secondary
)

还可以设置背景色和进度条大小:

refreshLayout.setProgressBackgroundColorSchemeResource(R.color.white)
refreshLayout.setSize(SwipeRefreshLayout.LARGE)  // 或者 DEFAULT

我个人习惯把颜色控制在 2-3 种以内。颜色太多反而显得杂乱,用户看着也眼花。

3. 避坑指南:我曾经踩过的三个坑

下拉刷新看起来简单,但实际项目中容易出问题。我把自己踩过的坑整理一下,你们可以少走弯路。

坑一:刷新时列表自动滚动到顶部

这个问题很隐蔽。当你调用 adapter.submitList() 时,如果数据完全替换了旧数据,RecyclerView 可能会自动滚动到顶部。用户正在看第 20 条,刷新完突然跳回第 1 条,体验很差。

解决方案:使用 DiffUtil 做局部更新,或者刷新后手动恢复滚动位置。

坑二:SwipeRefreshLayout 与 NestedScrollView 冲突

如果你把 SwipeRefreshLayout 和 NestedScrollView 嵌套使用,下拉手势可能会被 NestedScrollView 吃掉。我曾经在一个详情页里同时用了这两个,折腾了半天才发现是嵌套滑动冲突。

解决方案:要么不用 NestedScrollView,要么自定义 SwipeRefreshLayout 重写 onInterceptTouchEvent。

坑三:刷新动画卡住不消失

这个最常见。原因通常是:网络请求失败、数据为空、或者异步回调没有执行到 isRefreshing = false。我建议在 try-catch 的 finally 块里确保关闭刷新动画,或者设置一个超时机制。

4. 核心逻辑流程图

下面这张图,我把下拉刷新的完整流程画出来了。从用户手指下拉,到数据刷新完成,每一步都标清楚了。

下拉刷新核心流程 用户手指下拉 SwipeRefreshLayout 检测到下拉 触发 onRefresh 回调 执行数据加载(网络/数据库) isRefreshing = false 停止动画

5. 完整代码示例

光说不练假把式。下面是一个完整的 Fragment 实现,包含了下拉刷新、数据加载、错误处理:

class NewsFragment : Fragment() {

    private lateinit var refreshLayout: SwipeRefreshLayout
    private lateinit var recyclerView: RecyclerView
    private val adapter = NewsAdapter()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val view = inflater.inflate(R.layout.fragment_news, container, false)
        refreshLayout = view.findViewById(R.id.refreshLayout)
        recyclerView = view.findViewById(R.id.recyclerView)

        setupRecyclerView()
        setupRefreshLayout()
        loadData()

        return view
    }

    private fun setupRecyclerView() {
        recyclerView.layoutManager = LinearLayoutManager(requireContext())
        recyclerView.adapter = adapter
    }

    private fun setupRefreshLayout() {
        refreshLayout.setColorSchemeResources(R.color.purple_500, R.color.teal_200)
        refreshLayout.setOnRefreshListener {
            loadData()
        }
    }

    private fun loadData() {
        // 模拟网络请求
        viewLifecycleOwner.lifecycleScope.launch {
            try {
                val data = withContext(Dispatchers.IO) {
                    // 这里替换成真实的网络请求
                    fetchNewsFromServer()
                }
                adapter.submitList(data)
            } catch (e: Exception) {
                Toast.makeText(requireContext(), "刷新失败,请重试", Toast.LENGTH_SHORT).show()
            } finally {
                // 确保动画一定会关闭
                refreshLayout.isRefreshing = false
            }
        }
    }
}
小技巧:如果你用的是 Kotlin 协程,记得把 isRefreshing = false 放在 finally 块里。这样无论请求成功还是失败,动画都能正常关闭。我习惯在所有异步操作里都用这个模式,省心。

6. 性能与体验优化

下拉刷新虽然简单,但有几个细节能显著提升用户体验:

  • 刷新阈值:SwipeRefreshLayout 默认下拉超过 64dp 才触发刷新。这个值一般不用改,但如果你的列表项比较密集,可以适当调小。
  • 刷新时禁用滑动:刷新过程中,最好禁用 RecyclerView 的滑动,防止用户边刷新边滑动导致界面错乱。可以用 refreshLayout.isEnabled = false 临时禁用。
  • 空状态处理:如果刷新后数据为空,不要只显示一个空白列表。加个空状态提示,比如「暂无新内容」。我见过太多 App 刷新完一片空白,用户完全不知道发生了什么。

下拉刷新这个功能,说白了就是「监听手势 → 加载数据 → 更新界面」这三步。但真正做好,需要你在细节上多花心思。嗯,今天就先聊到这里。代码写完了,记得跑一遍测试,确保刷新动画能正常关闭——这个坑我已经替你们踩过了。


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