【开源项目】Compose版SmartRefreshLayout,了解一下~
下拉刷新是我们开发中的常见的需求,官方提供了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,收藏~
作者:RicardoMJiang
链接:https://juejin.cn/post/7016306653892968484
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。