文章目录
- 一、SwipeRefreshLayout 的定位
- 二、布局使用详解
- 2.1 布局骨架
- 2.2 必须遵守的两条布局规则
- 三、代码接入:基本三步
- 四、设置方法详解(API)
- 4.1 状态控制
- 4.2 监听器设置
- 4.3 外观定制
- 4.4 转圈位置与触发距离
- 4.5 嵌套滚动
- 五、进阶细节:仅在滚动到顶部时允许刷新
- 六、常见问题
- 七、小结
下拉刷新(Pull-to-Refresh)是移动端最常见的交互之一:用户在可滚动区域的顶部继续下拉,松手后触发数据重新加载,顶部出现转圈动画作为加载反馈。Android 官方实现为SwipeRefreshLayout(androidx.swiperefreshlayout.widget),本文结合天气 App 的实际用法,从布局结构到全部常用设置方法逐一详解。
一、SwipeRefreshLayout 的定位
SwipeRefreshLayout继承自ViewGroup,是官方提供的一个通用容器控件,自身不绘制业务内容,职责只有两件:
- 监听手势:当子 View 已滚动到顶部、用户继续向下拖动时,判定为一次"刷新意图";
- 状态反馈:绘制转圈动画,手势结束时回调刷新监听器,由监听器执行真正的数据加载。
因此布局上它必须作为最外层容器存在,把需要支持刷新的内容整体包裹起来。
二、布局使用详解
2.1 布局骨架
以我的天气 App 为例,页面结构是:
SwipeRefreshLayout // 最外层:下拉刷新的载体 └─ ConstraintLayout // 页面内容容器 ├─ 顶部标题栏(城市名、搜索按钮) └─ ScrollView // 可滚动正文 └─ 温度、湿度、逐小时预报等内容<androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:id="@+id/swipe_refresh"android:layout_width="match_parent"android:layout_height="match_parent"><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv_city_name"android:layout_width="wrap_content"android:layout_height="wrap_content"/><ScrollViewandroid:id="@+id/scroll_view"android:layout_width="0dp"android:layout_height="0dp"android:fillViewport="true"android:overScrollMode="never"android:scrollbars="none"><!-- 天气正文内容 --></ScrollView></androidx.constraintlayout.widget.ConstraintLayout></androidx.swiperefreshlayout.widget.SwipeRefreshLayout>2.2 必须遵守的两条布局规则
规则一:SwipeRefreshLayout 只能有一个直接子 View。
它需要明确"该监听哪个 View 的滚动"。若内容由多个控件并列组成,必须先包一层容器(LinearLayout/ConstraintLayout/ScrollView/RecyclerView)再作为唯一直接子 View;直接放置两个及以上子 View,运行期会抛出:
IllegalArgumentException: SwipeRefreshLayout can host only one direct child规则二:子内容必须是可滚动的。
下拉刷新手势的前提是"子 View 已滚到顶部(无法继续上滚)后仍向下拉"。若内容短于一屏、本身不能滚动,手势不成立,下拉不会有反应。所以内部通常使用ScrollView或RecyclerView。
三、代码接入:基本三步
publicclassMainActivityextendsAppCompatActivity{privateSwipeRefreshLayoutswipeRefresh;@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);swipeRefresh=findViewById(R.id.swipe_refresh);// ① 注册刷新监听:下拉松手后触发数据重新加载swipeRefresh.setOnRefreshListener(this::startWeather);}// ② 刷新方法:亮出转圈 → 发起请求privatevoidstartWeather(){swipeRefresh.setRefreshing(true);// 显示转圈// ... 发起网络请求(OkHttp 等)...}// ③ 请求结束(无论成功失败)必须收圈privatevoidonDataLoaded(){swipeRefresh.setRefreshing(false);// 隐藏转圈// ... 更新 UI ...}}最容易犯的错误:忘记第 ③ 步。如果请求失败路径没有调setRefreshing(false),转圈将永久显示,用户会认为应用卡死。正确写法应把收圈逻辑放在成功与失败两个回调里都执行。
忘记关闭的效果如下:
正确效果如下:
四、设置方法详解(API)
4.1 状态控制
| 方法 | 说明 |
|---|---|
setRefreshing(boolean refreshing) | 程序化控制转圈显隐。传true立即显示转圈且不播放进入动画(常用于代码主动刷新);传false隐藏 |
isRefreshing() | 返回当前是否处于刷新状态 |
setEnabled(boolean enabled) | 是否允许下拉刷新手势。常用于"仅滚动到顶部时允许刷新" |
isEnabled() | 当前是否允许下拉手势 |
示例:进入页面时自动刷新一次(无需用户手势):
swipeRefresh.setRefreshing(true);startWeather();4.2 监听器设置
①setOnRefreshListener(OnRefreshListener listener)
刷新监听,内部只定义了一个方法onRefresh(),在下拉松手(或程序触发)时回调:
swipeRefresh.setOnRefreshListener(()->{// 在这里重新加载数据fetchWeather();});注意onRefresh()回调发生时,转圈已经由控件自动显示,不需要也不应在回调里手动调setRefreshing(true);回调只负责发起加载。
②setOnRefreshListener(OnChildScrollUpCallback callback, OnRefreshListener listener)
带"是否可以上滚回调"的版本,OnChildScrollUpCallback用于自定义"何时判定为已到顶部",适用于内置判断不满足需求的场景:
swipeRefresh.setOnRefreshListener(newSwipeRefreshLayout.OnChildScrollUpCallback(){@OverridepublicbooleancanChildScrollUp(SwipeRefreshLayoutparent,Viewchild){// 返回 true 表示"子 View 还可以继续上滚",此时不触发刷新returnchild.canScrollVertically(-1);}},()->fetchWeather());③setOnChildScrollUpCallback(OnChildScrollUpCallback callback)
仅设置上滚判定回调(不重复注册刷新监听),可动态更换判定逻辑:
swipeRefresh.setOnChildScrollUpCallback((parent,child)->child.getScrollY()>0);// 例如:ScrollView 滚动过就禁止刷新4.3 外观定制
① 转圈颜色 ——setColorSchemeColors(int... colors)
设置转圈动画的颜色序列,转圈会按传入颜色依次渐变:
swipeRefresh.setColorSchemeColors(ContextCompat.getColor(this,R.color.colorPrimary),ContextCompat.getColor(this,R.color.colorAccent),0xFFFF3333);② 转圈颜色 ——setColorSchemeResources(int... colorResIds)(等价资源写法)
swipeRefresh.setColorSchemeResources(R.color.blue,R.color.green,R.color.orange);③ 转圈背景圆盘颜色
转圈默认显示在白色圆盘上;若界面背景为深色,可自定义圆盘颜色:
swipeRefresh.setProgressBackgroundColorSchemeColor(ContextCompat.getColor(this,R.color.white));// 或资源写法:swipeRefresh.setProgressBackgroundColorSchemeResource(R.color.white);④ 转圈尺寸 ——setSize(int size)
SwipeRefreshLayout.SIZE_DEFAULT(默认)与SIZE_LARGE(大号)二选一:
swipeRefresh.setSize(SwipeRefreshLayout.SIZE_LARGE);改动效果如下:
4.4 转圈位置与触发距离
下拉时转圈会跟随手指下移,可通过偏移方法调整它的初始位置与跟随范围。
① 设置转圈起始偏移 ——setProgressViewOffset(boolean scale, int start, int end)
scale:是否以缩放动画出现;start:转圈在父容器中的起始偏移(通常设为触发下拉前的停留位置,一般取一个屏幕高度如screenHeightPx,让它在手指下拉时才出现);end:转圈完全出现后的结束偏移。
intscreenHeightPx=getResources().getDisplayMetrics().heightPixels;swipeRefresh.setProgressViewOffset(false,screenHeightPx/3,screenHeightPx/4);② 设置转圈距离父容器顶部的初始距离 ——setProgressViewEndTarget(boolean scale, int end)
只指定转圈出现后的最终位置:
swipeRefresh.setProgressViewEndTarget(false,dp2px(96));③ 下拉触发刷新的最小距离 ——setDistanceToTriggerSync(int distance)
手指下拉超过该像素距离(松手)才触发刷新。默认值由系统计算(大致为一个屏幕高度),数值越小越"灵敏":
swipeRefresh.setDistanceToTriggerSync(dp2px(120));④ 松手回弹的最大下拉距离 ——setSlingshotDistance(int distance)
限制转圈在手指持续下拉时能跟随的最大位移(AndroidX 1.1.0+):
swipeRefresh.setSlingshotDistance(dp2px(80));辅助换算方法:
privateintdp2px(floatdp){return(int)(dp*getResources().getDisplayMetrics().density+0.5f);}4.5 嵌套滚动
SwipeRefreshLayout默认开启了嵌套滚动支持,可被外层NestedScrollView/CoordinatorLayout正确协调。只有内容自身实现了复杂拖动逻辑(如自绘RecyclerView拖拽排序)时才可能需要关闭:
swipeRefresh.setNestedScrollingEnabled(false);五、进阶细节:仅在滚动到顶部时允许刷新
页面内容较长时,用户滚到中间继续下拉,预期是"继续浏览"而非"刷新"。若不加限制会打断浏览体验,因此应只在内容处于顶部时开启刷新。
对ScrollView,监听滚动位置:
scrollView.setOnScrollChangeListener((v,scrollX,scrollY,oldScrollX,oldScrollY)->swipeRefresh.setEnabled(scrollY==0));scrollY == 0表示尚未向下滚动,此时允许下拉刷新;一旦滚离顶部立即禁用刷新,把下拉手势交还给滚动。
对RecyclerView,利用 LayoutManager 判断首项是否完全可见:
recyclerView.addOnScrollListener(newRecyclerView.OnScrollListener(){@OverridepublicvoidonScrolled(@NonNullRecyclerViewrv,intdx,intdy){LinearLayoutManagerlm=(LinearLayoutManager)rv.getLayoutManager();booleanatTop=lm!=null&&lm.findFirstCompletelyVisibleItemPosition()==0;swipeRefresh.setEnabled(atTop);}});如果没有设置刷新条件,会出现不能向上滑,而只能刷新的情况:
六、常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
运行期崩溃:can host only one direct child | 直接子 View 多于一个 | 先包一层容器,只保留一个直接子 View |
| 转圈一直不消失 | 刷新结束后没调setRefreshing(false) | 成功与失败回调里都要收圈 |
| 内容滚到一半下拉也弹转圈 | 未做顶部判断 | setEnabled(scrollY == 0)或设置OnChildScrollUpCallback |
| 下拉没反应 | 内部不是可滚动控件,手势不成立 | 换成ScrollView/RecyclerView |
| 转圈颜色与主题不搭 | 未自定义 | setColorSchemeResources(...) |
| 深色背景看不到转圈 | 白色圆盘与背景融为一体 | setProgressBackgroundColorSchemeResource(...)改圆盘颜色 |
与CoordinatorLayout冲突/手势异常 | 嵌套滚动协调问题 | 检查setNestedScrollingEnabled(true)是否被误关 |
七、小结
下拉刷新的接入路径清晰稳定:
布局:SwipeRefreshLayout(唯一子 View = 可滚动内容容器) ↓ 代码:setOnRefreshListener 注册刷新回调 ↓ 刷新中:转圈由控件自动显示(setRefreshing(true) 可程序化触发) ↓ 结束:setRefreshing(false) 必须覆盖成功与失败两条路径控件负责手势监听、转圈动画与状态机,真正的业务复杂度(网络请求、解析、UI 更新)全部收在onRefresh回调里——这是 Android 官方把通用交互沉淀为标准控件的典型设计。在此基础上,善用setColorSchemeResources、setDistanceToTriggerSync、OnChildScrollUpCallback等配置方法,即可做出与产品风格一致、交互手感合适的下拉刷新体验。