Pull-to-Refresh:基础,RefreshControl和UIRefreshControl

作者: IT Sectr 发布日期: 2026-02-27 阅读时间: 8 分钟
Pull-to-Refresh — 一种移动界面模式,用户用手指向下拉动列表,触发加载新鲜数据。该手势伴随有视觉指示器 — 旋转的spinner或动画图标 — 在加载完成后消失。根据Apple HIG的UX分析,自Tweetie(2008)中引入以及随后Apple和Google对其进行标准化以来,Pull-to-Refresh已成为新闻源、社交网络和邮件客户端中内容更新的标准机制。

要点

  • Pull-to-Refresh — 向下拖动列表以刷新数据的手势,伴随有视觉加载指示器。
  • 在iOS中使用UIRefreshControl(iOS 6+),通过refreshControl属性添加到UITableViewController或UIScrollView。
  • 在Android中使用SwipeRefreshLayout(来自Support Library)— 用于RecyclerView或NestedScrollView的ViewGroup包装器。
  • 两个API都支持通过listener自定义颜色、指示器和回调(iOS:UIRefreshControl.target-action,Android:setOnRefreshListener)。
  • 当列表不在顶部位置时,Pull-to-Refresh会自动锁定 — 与滚动的冲突在架构上已被排除。

什么是Pull-to-Refresh?

Pull-to-Refresh — 一种用户界面模式,用户向下拖动(pull down)列表或可滚动区域来刷新内容。视觉上,该手势伴随有一个加载指示器(spinner),出现在屏幕顶部,在收到数据后消失。该模式由iPhone版Tweetie应用(2008)推广,随后由Apple(iOS 6 — UIRefreshControl)和Google(Android Support Library — SwipeRefreshLayout)标准化。

从技术角度看,Pull-to-Refresh是平移跟踪(追踪手指位移)和达到阈值时触发的组合。用户向下拖动列表,克服阻力( resistive overscroll),在超过阈值后(iOS约80px,Android约64dp),指示器动画和异步加载启动。如果用户在阈值前释放手指 — 列表返回初始位置而不进行更新。

根据Material Design Guidelines,Pull-to-Refresh 不应用于导航或切换标签页 — 其唯一目的是刷新数据。在IT Sectr,我们将Pull-to-Refresh应用于新闻源、订单列表和聊天中,这些地方数据的时效性对用户体验至关重要。

iOS中的Pull-to-Refresh:UIRefreshControl

UIRefreshControl — iOS中用于Pull-to-Refresh的标准控件,自iOS 6起可用。UIRefreshControl通过refreshControl属性(iOS 10+)添加到UITableViewController,或在更早版本中作为表的子视图添加。它包含一个内置的spinner,具有可配置的颜色(tintColor)、title属性和带标签的属性字符串(例如「正在刷新...」)。

UIRefreshControl通过target-action机制工作:手势激活时,调用指定的方法(例如refresh(_:))。在方法内部执行异步数据加载。完成后,调用endRefreshing(),以动画方式隐藏指示器。UIRefreshControl自动管理手势灵敏度 — 仅在表格顶部位置(contentOffset.y <= 0)时触发。

tintColor属性设置spinner的颜色。title属性允许在完成后显示「2分钟前更新」的文本。从iOS 10开始,UIRefreshControl支持通过UIActivityIndicatorView或持久化自定义视图进行自定义动画。在IT Sectr,我们将tintColor调整为品牌色,并通过attributedTitle显示上次更新时间 — 这增强了用户对数据的信任。

Android中的Pull-to-Refresh:SwipeRefreshLayout

SwipeRefreshLayout — 来自Android Support Library(androidx.swiperefreshlayout)的ViewGroup,包装可滚动内容(RecyclerView、NestedScrollView、ListView)并添加Pull-to-Refresh功能。与UIRefreshControl(它是控件而非容器)不同,SwipeRefreshLayout是一个容器,它拦截子视图的触摸事件,并在超过阈值时触发刷新指示器。

SwipeRefreshLayout使用Material Design圆形进度指示器,通过setColorSchemeColors()设置颜色。setOnRefreshListener方法设置onRefresh()回调,在其中执行异步加载。完成后,调用setRefreshing(false)隐藏指示器。重要:setRefreshing(true)会再次调用onRefresh() — 因此要以编程方式启动刷新,请使用标志或post方法。

setProgressBackgroundColorSchemeResource属性更改指示器的背景。setSize(SwipeRefreshLayout.LARGE) — spinner的大小。在XML布局中,SwipeRefreshLayout包装RecyclerView:swipe_refresh_layout → recycler_view。根据Google I/O 2024,SwipeRefreshLayout在85%的带内容源的Android应用中使用。在IT Sectr,我们将所有具有异步加载列表的屏幕包装在SwipeRefreshLayout中 — 这确保了在所有Android版本上统一的用户体验。

Material Pull-to-Refresh(Android 12+)

从Android 12(Material You)开始,Google推荐使用来自material-1.6.0+库(Compose使用androidx.compose.material3.pulltorefresh)的新版Material Pull-to-Refresh。新API使用带有spring动画支持和基于壁纸的自适应颜色的动画指示器。SwipeRefreshLayout对Android 12以下版本保持兼容。

最佳实践和常见错误

Pull-to-Refresh — 容易实现的模式,但包含几个降低用户体验的典型错误。让我们看看它们以及预防方法。

  • 重复刷新 — 用户可能在加载完成前多次拖动列表。解决方案:在启动时设置isRefreshing标志,并在onRefresh()中检查。在iOS中,endRefreshing()仅在完成后调用;UIRefreshControl中的手势锁定是内置的。
  • 缺乏反馈 — 加载指示器应在用户超过阈值后出现。不要在触摸时立即显示指示器 — 这会令人困惑。iOS和Android会自动处理。
  • 忽略刷新时间 — 如果数据在200毫秒内刷新完成,指示器应至少显示500毫秒,以便用户注意到刷新。UIRefreshControl有最小动画时间;在Android中使用Handler.postDelayed设置最小显示时间。
  • 与键盘冲突 — 键盘打开时,Pull-to-Refresh可能意外触发。在手势开始时,通过iOS中的view.endEditing(true)和Android中的InputMethodManager.hideSoftInputFromWindow()隐藏键盘。
  • 非刷新用途 — 不要将Pull-to-Refresh用于导航(切换标签页、返回)。这违反了两个平台的HIG并会使用户困惑。

在IT Sectr,我们在测试服务器日志中发现重复请求后,在每个项目中添加了isRefreshing检查 — 结果发现手指快的用户会连续触发刷新多达3次。

Swift和Kotlin代码示例

示例1:iOS中的UIRefreshControl(Swift)

向UITableViewController添加带有自定义spinner颜色和attributed title的Pull-to-Refresh。数据加载完成后隐藏指示器。

swift
import UIKit

class FeedTableViewController: UITableViewController {

    private var items: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.refreshControl = UIRefreshControl()
        refreshControl?.tintColor = .systemBlue
        refreshControl?.attributedTitle = NSAttributedString(
            string: “下拉以刷新”
        )
        refreshControl?.addTarget(
            self,
            action: #selector(refreshData),
            for: .valueChanged
        )
    }

    @objc private func refreshData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            self.items = FeedService().fetchLatest()
            self.tableView.reloadData()
            self.refreshControl?.endRefreshing()
        }
    }
}

tableView.refreshControl属性(iOS 10+)设置UIRefreshControl。带有.valueChanged事件的addTarget在手势激活时触发。endRefreshing()是必需的 — 没有它,指示器将无限旋转。异步加载通过DispatchQueue.main.asyncAfter模拟 — 在实际项目中会是URLSession或async/await。

示例2:Android中的SwipeRefreshLayout(Kotlin)

将RecyclerView包装在具有自定义指示器颜色的SwipeRefreshLayout中。onRefresh启动加载并在完成后隐藏指示器。

kotlin
class FeedFragment : Fragment() {

    private var _binding: FragmentFeedBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentFeedBinding.inflate(inflater, container, false)

        binding.swipeRefreshLayout.setColorSchemeColors(
            resources.getColor(R.color.brand_blue, null),
            resources.getColor(R.color.brand_green, null)
        )
        binding.swipeRefreshLayout.setOnRefreshListener {
            loadData()
        }
        return binding.root
    }

    private fun loadData() {
        viewModelScope.launch {
            try {
                val result = repository.getLatestFeed()
                adapter.submitList(result)
            } finally {
                binding.swipeRefreshLayout.isRefreshing = false
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

setColorSchemeColors设置旋转的Material Design指示器的颜色。isRefreshing = false必须在finally中调用,以便即使在加载错误时也能隐藏指示器。ViewModelScope.launch在fragment的生命周期中执行协程 — 当fragment被销毁时,协程自动取消,防止内存泄漏。

示例3:SwiftUI .refreshable(iOS 15+)

现代SwiftUI提供了.refreshable修饰符,可自动向List或ScrollView添加Pull-to-Refresh。

swift
import SwiftUI

struct FeedView: View {

    @State private var items: [String] = []

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
        .refreshable {
            items = await FeedService().fetchLatestAsync()
        }
    }
}

.refreshable修饰符接受一个async-closure,在Pull-to-Refresh时执行。SwiftUI自动显示和隐藏刷新指示器,管理竞态条件(在当前加载完成前不启动重新加载)并根据平台调整动画。对于iOS 15+,这是在SwiftUI中实现Pull-to-Refresh的preferred方式。

常见问题

Pull-to-Refresh在SwiftUI中有效吗?

是的,SwiftUI为List或ScrollView提供了.refreshable修饰符,自iOS 15起可用。在闭包内执行异步数据加载代码。SwiftUI自动管理刷新指示器,并在当前加载完成前阻止重新触发 — 这是新项目的标准recommended方法。

如何防止重复刷新?

使用isRefreshing标志:在加载开始时设置为true,完成后设置为false。在iOS中,UIRefreshControl会自动阻止重复调用,直到调用endRefreshing()。在Android中,在onRefresh()开始时检查SwipeRefreshLayout.isRefreshing:如果为true — return。这保证每个手势一次请求。

Pull-to-Refresh与列表滚动冲突吗?

UIRefreshControl和SwipeRefreshLayout仅在列表顶部位置(contentOffset == 0)激活。架构排除了冲突:只要列表滚动了哪怕1px,Pull-to-Refresh手势就不会激活。如果发生冲突 — 检查Android中的nestedScrollingEnabled或拦截触摸的自定义GestureRecognizer。

总结

  • Pull-to-Refresh — 通过向下拖动列表来刷新数据的模式,由Apple和Google在所有移动平台上标准化。
  • iOS中的UIRefreshControl — 控件,具有target-action、tintColor、attributedTitle和必需的endRefreshing()。
  • Android中的SwipeRefreshLayout — ViewGroup容器,具有setOnRefreshListener、setColorSchemeColors和isRefreshing。
  • Material Pull-to-Refresh(Android 12+)— 带有spring动画的新API,推荐用于新项目。
  • SwiftUI .refreshable — 声明式修饰符,带有异步闭包,自iOS 15起可用。
  • isRefreshing标志防止重复刷新 — 在两个平台上都是必需的。
  • Pull-to-Refresh不应用于导航 — 仅用于根据Material Design和Apple HIG更新内容。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读