用Compose实现一个自己的下拉刷新控件

d76d5fd238025ba484809bc20b699155.png

/   今日科技快讯   /

近日,在纽约市纽约大学朗格尼健康中心进行了一场特殊的手术,这是人类首次将猪肾移植到人体中且没有立即引发受体免疫系统的排斥反应。这是一项潜在的重大进步,最终可能有助于缓解用于移植的人体器官严重短缺的问题。

/   作者简介   /

本篇文章来自RicardoMJiang的投稿,文章主要分享了他用Compose实现的一个下拉刷新的组件SmartRefreshLayout的过程,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

RicardoMJiang的博客地址:

https://juejin.cn/user/668101431009496

/   前言   /

下拉刷新是我们开发中的常见的需求,官方提供了SwipeRefreshLayout来实现下拉刷新,但我们常常需要定制Header或者Header与内容一起向下滚动,因此SwipeRefreshLayout往往不能满足我们的需求。


在使用XML开发时,Github上有不少开源库如 SmartRefreshLayout 实现了下拉刷新功能,可以方便地定制化Header与滚动方式。

SmartRefreshLayout :

https://github.com/scwang90/SmartRefreshLayout

本文主要介绍如何开发一个简单易用的Compose版SmartRefreshLayout,快速实现下拉刷新功能,如果对您有所帮助可以点个Star。

Compose版SmartRefreshLayout:

https://github.com/shenzhen2017/compose-refreshlayout

/   效果图   /

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

基本使用自定义Header

9c65e6d179567a6b6a9a8baed5a3355a.gif

867e5951a5747be547a574d0ae9288c8.gif

Lottie HeaderFixedBehind(固定在背后)

9d745d32b829ec636e14dee47247075f.gif

95a1cdb4df2faf868c01de6e2a3928f1.gif

FixedFront(固定在前面)FixedContent(内容固定)

034348b6f263ecb7fa718b4e20353ee0.gif

24f4ce5cacf8b1e9f6bbbe08d3815811.gif

/   特性   /

  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使用。

/   主要原理   /

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

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

https://github.com/shenzhen2017/compose-refreshlayout

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android还可以外接摄像头?一起来学习一下吧

Android 12 正式发布 | 开发者们的全新舞台

欢迎关注我的公众号

学习技术或投稿

b1ba3aeae4ea9be6beb4e58da3d13600.png

14d5e6f1fd33e3213551be8a98ed7ece.png

长按上图,识别图中二维码即可关注

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值