前言
[DEPRECATED]下拉刷新是我们开发中的常见的需求,官方提供了SwipeRefreshLayout
来实现下拉刷新,但我们常常需要定制Header
或者Header
与内容一起向下滚动,因此SwipeRefreshLayout
往往不能满足我们的需求
在使用XML
开发时,Github
上有不少开源库如SmartRefreshLayout实现了下拉刷新功能,可以方便地定制化Header
与滚动方式
本文主要介绍如何开发一个简单易用的Compose
版SmartRefreshLayout
,快速实现下拉刷新功能,如果对您有所帮助可以点个Star
:Compose版SmartRefreshLayout
效果图
我们首先看下最终的效果图
基本使用 | 自定义Header |
---|---|
Lottie Header | FixedBehind(固定在背后) |
---|---|
FixedFront(固定在前面) | FixedContent(内容固定) |
---|---|
特性
- 接入方便,使用简单,快速实现下拉刷新功能
- 支持自定义
Header
,Header
可观察下拉状态并更新UI
- 自定义
Header
支持Lottie
,并支持观察下拉状态开始与暂停动画 - 支持自定义
Translate
,FixedBehind
,FixedFront
,FixedContent
等滚动方式 - 支持与
Paging
结合实现上滑加载更多功能
使用
接入
第 1 步:在工程的build.gradle
中添加:
allprojects {
repositories {
...
mavenCentral()
}
}
第2步:在应用的build.gradle
中添加:
dependencies {
implementation 'io.github.shenzhen2017:compose-refreshlayout:1.0.0'
}
简单使用
SwipeRefreshLayout
函数主要包括以下参数:
isRefreshing
: 是否正在刷新onRefresh
: 触发刷新回调modifier
: 样式修饰符swipeStyle
: 下拉刷新方式swipeEnabled
: 是否允许下拉刷新refreshTriggerRate
: 刷新生效高度与indicator
高度的比例maxDragRate
: 最大刷新距离与indicator
高度的比例indicator
: 自定义的indicator
,有默认值
在默认情况下,我们只需要传入isRefreshing
(是否正在刷新)与onRefresh
触发刷新回调两个参数即可
@Composable
fun BasicSample() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(2000)
refreshing = false
}
}
SwipeRefreshLayout(isRefreshing = refreshing, onRefresh = { refreshing = true }) {
//...
}
}
如上所示:在触发刷新回调时将refreshing
设置为true
,并在刷新完成后设置为false
即可实现简单的下拉刷新功能
Header
自定义SwipeRefreshLayout
支持传入自定义的Header
,如下所示:
@Composable
fun CustomHeaderSample() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(2000)
refreshing = false
}
}
SwipeRefreshLayout(
isRefreshing = refreshing,
onRefresh = { refreshing = true },
indicator = {
BallRefreshHeader(state = it)
}) {
//...
}
}
如上所示:BallRefreshHeader
即为自定义的Header
,Header
中会传入SwipeRefreshState
,我们通过SwipeRefreshState
可获得以下参数
isRefreshing
: 是否正在刷新isSwipeInProgress
: 是否正在滚动maxDrag
: 最大下拉距离refreshTrigger
: 刷新触发距离headerState
: 刷新状态,包括PullDownToRefresh
,Refreshing
,ReleaseToRefresh
三个状态indicatorOffset
:Header
偏移量
这些参数都是MutableState
我们可以观察这些参数的变化以实现Header UI
的更新
Lottile Header
自定义Compose
目前已支持Lottie
,我们接入Lottie
依赖后,就可以很方便地实现一个Lottie Header
,并且在正在刷新时播放动画,其它时间暂停动画,示例如下:
@Composable
fun LottieHeaderOne(state: SwipeRefreshState) {
var isPlaying by remember {
mutableStateOf(false)
}
val speed by remember {
mutableStateOf(1f)
}
isPlaying = state.isRefreshing
val lottieComposition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.refresh_one),
)
val lottieAnimationState by animateLottieCompositionAsState(
composition = lottieComposition, // 动画资源句柄
iterations = LottieConstants.IterateForever, // 迭代次数
isPlaying = isPlaying, // 动画播放状态
speed = speed, // 动画速度状态
restartOnPlay = false // 暂停后重新播放是否从头开始
)
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(), contentAlignment = Alignment.Center
) {
LottieAnimation(
lottieComposition,
lottieAnimationState,
modifier = Modifier.size(150.dp)
)
}
}
自定义下滑方式
SwipeRefreshLayout
支持以下4种下滑方式
enum class SwipeRefreshStyle {
Translate, //平移,即内容与Header一起向下滑动,Translate为默认样式
FixedBehind, //固定在背后,即内容向下滑动,Header不动
FixedFront, //固定在前面, 即Header固定在前,Header与Content都不滑动
FixedContent //内容固定,Header向下滑动,即官方样式
}
如上所示,其中默认方式为Translate
,即内容与Header
一起向下滑动
各位可根据需求选择相应的下滑方式,比如要实现类似官方的下滑效果,即可使用FixedContent
上拉加载更多
在Compose
中,上拉加载更多直接使用Paging3
看起来已经足够用了,因此本库没有实现上拉加载更多相关功能
因此如果想要实现上拉加载更多,可自行结合Paging3
使用
主要原理
下拉刷新功能,其实主要是嵌套滚动的问题,我们将Header
与Content
放到一个父布局中统一管理,然后需要做以下事
- 当我们的手指向下滚动时,首先交由
Content
处理,如果Content
滚动到顶部了,再交由父布局处理,然后父布局根据手势进行一定的偏移,增加offset
- 当我们松手时,判断偏移的距离,如果大于刷新触发距离则触发刷新,否则回弹到顶部(
offset
置为0) - 当我们手指向上滚动时,首先交由父布局处理,如果父布局的
offset
>0则由父布局处理,减少offset
,否则则由Content
消费手势
NestedScrollConnection
介绍
为了实现上面说的需求,我们需要对滚动进行拦截,Compose
提供了NestedScrollConnection
来实现嵌套滚动
interface NestedScrollConnection {
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = Offset.Zero
suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
suspend fun onPostFling(consumed: Velocity, available: Velocity) = return Velocity.Zero
}
如上所示,NestedScrollConnection
主要提供了4个接口
onPreScroll
: 先拦截滑动事件,消费后再交给子布局onPostScroll
: 子布局处理完滑动事件后再交给父布局,可获取当前还剩下多少可用的滑动事件偏移量onPreFling
:Fling
开始前回调onPostFling
:Fling
完成后回调
Fling
含义:当我们手指在滑动列表时,如果是快速滑动并抬起,则列表会根据惯性继续飘一段距离后停下,这个行为就是Fling
,onPreFling
在你手指刚抬起时便会回调,而onPostFling
会在飘一段距离停下后回调。
具体实现
上面我们已经介绍了总体思路与NestedScrollConnection API
,然后我们应该需要重写以下方法
onPostScroll
: 当Content
滑动到顶部时,如果继续往下滑,我们就应该增加父布局的offset
,因此在onPostScroll
中判断available.y > 0
,然后进行相应的偏移,对我们来说是个合适的时机onPreScroll
: 当我们上滑时,如果offset>0
,则说明父布局有偏移,因此我们应先减小父布局的offset
直到0,然后将剩余的偏移量传递给Content
,因此下滑时应该使用onPreScroll
拦截判断onPreFling
: 当我们松开手时,应判断当前的偏移量是否大于刷新触发距离,如果大于则触发刷新,否则父布局的offset
置为0,这个判断在onPreFling
时做比较合适
具体实现如下:
internal class SwipeRefreshNestedScrollConnection() : NestedScrollConnection {
override fun onPreScroll(
available: Offset,source: NestedScrollSource
): Offset = when {
// 如果用户正在上滑,需要在这里拦截处理
source == NestedScrollSource.Drag && available.y < 0 -> onScroll(available)
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset,available: Offset,source: NestedScrollSource
): Offset = when {
// 如果用户正在下拉,在这里处理剩余的偏移量
source == NestedScrollSource.Drag && available.y > 0 -> onScroll(available)
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
//如果偏移量大于刷新触发距离,则触发刷新
if (!state.isRefreshing && state.indicatorOffset >= refreshTrigger) {
onRefresh()
}
//不消费速度,直接返回0
return Velocity.Zero
}
}
总结
本文主要介绍如何使用及实现一个Compose
版的SmartRefreshLayout
,它具有以下特性:
- 接入方便,使用简单,快速实现下拉刷新功能
- 支持自定义
Header
,Header
可观察下拉状态并更新UI
- 自定义
Header
支持Lottie
,并支持观察下拉状态开始与暂停动画 - 支持自定义
Translate
,FixedBehind
,FixedFront
,FixedContent
等滚动方式 - 支持与
Paging
结合实现上滑加载更多功能
项目地址
Compose版SmartRefreshLayout
开源不易,如果项目对你有所帮助,欢迎点赞,Star
,收藏~