【开源项目】Compose版SmartRefreshLayout,了解一下~

4,628

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

下拉刷新是我们开发中的常见的需求,官方提供了SwipeRefreshLayout来实现下拉刷新,但我们常常需要定制Header或者Header与内容一起向下滚动,因此SwipeRefreshLayout往往不能满足我们的需求
在使用XML开发时,Github上有不少开源库如 SmartRefreshLayout 实现了下拉刷新功能,可以方便地定制化Header与滚动方式
本文主要介绍如何开发一个简单易用的ComposeSmartRefreshLayout,快速实现下拉刷新功能,如果对您有所帮助可以点个Star: Compose版SmartRefreshLayout

效果图

我们首先看下最终的效果图

基本使用自定义Header
Lottie HeaderFixedBehind(固定在背后)
FixedFront(固定在前面)FixedContent(内容固定)

特性

  1. 接入方便,使用简单,快速实现下拉刷新功能
  2. 支持自定义Header,Header可观察下拉状态并更新UI
  3. 自定义Header支持Lottie,并支持观察下拉状态开始与暂停动画
  4. 支持自定义Translate,FixedBehind,FixedFront,FixedContent等滚动方式
  5. 支持与Paging结合实现上滑加载更多功能

使用

接入

第 1 步:在工程的build.gradle中添加:

allprojects {
	repositories {
		...
		mavenCentral()
	}
}

第2步:在应用的build.gradle中添加:

dependencies {
        implementation 'io.github.shenzhen2017:compose-refreshlayout:1.0.0'
}

简单使用

SwipeRefreshLayout函数主要包括以下参数:

  1. isRefreshing: 是否正在刷新
  2. onRefresh: 触发刷新回调
  3. modifier: 样式修饰符
  4. swipeStyle: 下拉刷新方式
  5. swipeEnabled: 是否允许下拉刷新
  6. refreshTriggerRate: 刷新生效高度与indicator高度的比例
  7. maxDragRate: 最大刷新距离与indicator高度的比例
  8. 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可获得以下参数

  1. isRefreshing: 是否正在刷新
  2. isSwipeInProgress: 是否正在滚动
  3. maxDrag: 最大下拉距离
  4. refreshTrigger: 刷新触发距离
  5. headerState: 刷新状态,包括PullDownToRefresh,Refreshing,ReleaseToRefresh三个状态
  6. 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使用

主要原理

下拉刷新功能,其实主要是嵌套滚动的问题,我们将HeaderContent放到一个父布局中统一管理,然后需要做以下事

  1. 当我们的手指向下滚动时,首先交由Content处理,如果Content滚动到顶部了,再交由父布局处理,然后父布局根据手势进行一定的偏移,增加offset
  2. 当我们松手时,判断偏移的距离,如果大于刷新触发距离则触发刷新,否则回弹到顶部(offset置为0)
  3. 当我们手指向上滚动时,首先交由父布局处理,如果父布局的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个接口

  1. onPreScroll: 先拦截滑动事件,消费后再交给子布局
  2. onPostScroll: 子布局处理完滑动事件后再交给父布局,可获取当前还剩下多少可用的滑动事件偏移量
  3. onPreFling: Fling开始前回调
  4. onPostFling: Fling完成后回调

Fling含义:当我们手指在滑动列表时,如果是快速滑动并抬起,则列表会根据惯性继续飘一段距离后停下,这个行为就是 FlingonPreFling 在你手指刚抬起时便会回调,而 onPostFling 会在飘一段距离停下后回调。

具体实现

上面我们已经介绍了总体思路与NestedScrollConnection API,然后我们应该需要重写以下方法

  1. onPostScroll: 当Content滑动到顶部时,如果继续往下滑,我们就应该增加父布局的offset,因此在onPostScroll中判断available.y > 0,然后进行相应的偏移,对我们来说是个合适的时机
  2. onPreScroll: 当我们上滑时,如果offset>0,则说明父布局有偏移,因此我们应先减小父布局的offset直到0,然后将剩余的偏移量传递给Content,因此下滑时应该使用onPreScroll拦截判断
  3. 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,它具有以下特性:

  1. 接入方便,使用简单,快速实现下拉刷新功能
  2. 支持自定义Header,Header可观察下拉状态并更新UI
  3. 自定义Header支持Lottie,并支持观察下拉状态开始与暂停动画
  4. 支持自定义Translate,FixedBehind,FixedFront,FixedContent等滚动方式
  5. 支持与Paging结合实现上滑加载更多功能

项目地址

Compose版SmartRefreshLayout
开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~