在Android上使用Jetpack Compose定制下拉刷新

在Android上使用Jetpack Compose定制下拉刷新

在Jetpack Compose中向LazyList添加下拉刷新非常简单。说真的,只需几行代码。然而,默认的外观和感觉并不是那么令人满意。我们希望做得更好一些,类似于iOS版本:当用户向下拉动列表时,移动列表并向用户提供反馈,告诉用户如果继续下拉,列表将要刷新,并显示上次刷新的时间。我们还希望增加默认的刷新阈值,因为在向上滚动时我们意外地刷新了页面。

幸运的是,使用Compose实现这一点相当容易。在本文中,我将尝试展示如何构建一个简化的演示应用程序,如下所示:

我准备了一个简单的演示样本。我将尝试以下逐步解释它,但如果你想直接跳转到最终代码,这是样本的链接。

因此,使用Compose的默认基本下拉刷新实现如下所示:我们有一个pullRefreshState,我们将其作为修饰符传递给容器,并且有一个与之同步的PullRefreshIndicator

val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
val pullToRefreshState = rememberPullToRefreshState(
    refreshing = isRefreshing,
    onRefresh = {
        viewModel.refresh()
    }
)

Box(
    modifier = Modifier
        .pullToRefresh(pullToRefreshState),
    contentAlignment = Alignment.Center
) {
    LazyColumn {..}

    PullRefreshIndicator(
        isRefreshing,
        pullToRefreshState,
    )
}

首先,让我们增加默认的阈值,使用该库非常容易做到。rememberPullRefreshState有一个名为refreshThreshold的参数:

val pullToRefreshState = rememberPullToRefreshState(
    refreshing = isRefreshing,
    refreshThreshold = 120.dp,
    onRefresh = {
        viewModel.refresh()
    }
)

现在用户需要下拉更多才能进行刷新。这将修复在向上滚动时不期望的刷新。

我们将不再使用库提供的默认PullRefreshIndicator,可以将其移除。相反,我们将在列表顶部显示一个指示器,当用户向下拉时会将内容向下推。如果你想一下,这可以是一个简单的可组合元素,放在列表的顶部,随着用户向下拉屏幕,其高度会增加。

Column(
    modifier = Modifier
        .pullToRefresh(pullToRefreshState),
) {
    MyCustomPullToRefreshIndicator()
    LazyColumn {..}
}

指示器默认高度为0。当用户向下拉屏幕时,我们将同时增加此可组合元素的高度,从而将列表向下推。为了观察用户拉动屏幕的程度,我们可以使用:

pullToRefreshState.progress

这是一个百分比的浮点数,从默认位置开始为0,达到阈值时为1,甚至可以超出。如果你想将其转换为高度,只需将其乘以100即可:

Column(
    modifier = Modifier
        .pullToRefresh(pullToRefreshState),
) {
    MyCustomPullToRefreshIndicator(
        modifier = Modifier
            .fillMaxWidth()
            .height((pullToRefreshState.progress * 100).roundToInt().dp)
    )
    LazyColumn {..}
}

这足以在用户下拉列表时将其向下推。你可以在指示器的位置放置任何你喜欢的内容。但是在这个演示中,我们将构建以下行为:

  • 当用户首次下拉时,显示"下拉刷新",同时显示最后刷新的时间。
  • 当用户达到阈值时,显示"释放以刷新"。
  • 在刷新过程中,显示"正在刷新"并显示加载图标。
  • 刷新完成后,指示器消失,列表返回到原始位置。

为了轻松区分这些状态,我们使用了一个枚举:

enum class RefreshIndicatorState(@StringRes val messageRes: Int) {
    Default(R.string.pull_to_refresh_complete_label),
    PullingDown(R.string.pull_to_refresh_pull_label),
    ReachedThreshold(R.string.pull_to_refresh_release_label),
    Refreshing(R.string.pull_to_refresh_refreshing_label)
}

将所有这些放在一起,我们的下拉刷新指示器看起来像这样:

private const val maxHeight = 100

@Composable
fun PullToRefreshIndicator(
    modifier: Modifier = Modifier,
    indicatorState: RefreshIndicatorState,
    pullToRefreshProgress: Float,
    timeElapsed: String,
) {
    val heightModifier = when (indicatorState) {
        RefreshIndicatorState.PullingDown -> {
            Modifier.height(
                (pullToRefreshProgress * 100)
                    .roundToInt()
                    .coerceAtMost(maxHeight).dp,
            )
        }
        RefreshIndicatorState.ReachedThreshold -> Modifier.height(maxHeight.dp)
        RefreshIndicatorState.Refreshing -> Modifier.wrapContentHeight()
        RefreshIndicatorState.Default -> Modifier.height(0.dp)
    }
    Box(
        modifier = modifier
            .fillMaxWidth()
            .animateContentSize()
            .then(heightModifier)
            .padding(15.dp),
        contentAlignment = Alignment.BottomStart,
    ) {
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            Text(
                text = stringResource(indicatorState.messageRes),
                style = MaterialTheme.typography.labelMedium,
                color = Color.Black,
            )
            if (indicatorState == RefreshIndicatorState.Refreshing) {
                CircularProgressIndicator(
                    modifier = Modifier.size(16.dp),
                    color = Color.Black,
                    trackColor = Color.Gray,
                    strokeWidth = 2.dp,
                )
            } else {
                Text(
                    text = stringResource(R.string.last_updated, timeElapsed),
                    style = MaterialTheme.typography.labelSmall,
                    color = Color.Black,
                )
            }
        }
    }
}

因此,当用户下拉时,我们同时应用动态高度。我倾向于将其限制在最大100dp。你可以根据自己的喜好进行调整。请注意animateContentSize()修饰符,它提供了这些状态之间的平滑过渡。

timeElapsed是自上次刷新屏幕以来经过的时间。您可以跟踪刷新时间,计算现在和上次刷新时间之间的时间,并将其转换为相应的文本。在本文中,我不会详细介绍这一点,但你可以在示例中看到一个实现的例子。

indicatorState是上面提到的四种状态之一:Default、Pulling、ReachedThreshold、Refreshing。如果我们观察到pullRefreshState.progress大于0,那意味着用户正在向下拉。如果进度达到1,那就意味着用户已经达到阈值。0表示默认状态,没有下拉。

当用户在达到阈值后松开手指,它将进入刷新状态。该库已经为此提供了回调。我们可以使用库的onRefresh回调来更新我们的指示器状态为Refreshing

val refreshIndicatorState by viewModel.refreshIndicatorState.collectAsState()

val pullToRefreshState = rememberPullRefreshState(
    refreshing = refreshIndicatorState == RefreshIndicatorState.Refreshing,
    refreshThreshold = 140.dp,
    onRefresh = {
        // will start fetching data and also will update indicator state
        viewModel.refresh()
    })

LaunchedEffect(pullToRefreshState.progress) {
     when {
        pullToRefreshState.progress >= 1 -> {
            viewModel.updateRefreshState(RefreshIndicatorState.ReachedThreshold)
        }

       pullToRefreshState.progress > 0 -> {
            viewModel.updateRefreshState(RefreshIndicatorState.PullingDown)
        }
    }
}

val timeElapsedSinceLastRefresh by viewModel.lastRefreshText.collectAsState()

Column(
        modifier = Modifier
            .pullRefresh(pullToRefreshState),
    ) {
        PullToRefreshIndicator(
            modifier = modifier,
            uiState = refreshIndicatorState,
            pullToRefreshProgress = pullToRefreshState.progress,
            timeElapsed = timeElapsedSinceLastRefresh
        )
        LazyColumn {..}
    }

使这个功能正常工作的最后一步是在刷新完成时将指示器状态更改回默认状态。否则它将永远显示刷新中。在哪里做这件事?这取决于你的情况。在这个示例中,我在viewmodel中完成了这个操作,当结果到达时(包括成功和错误的情况)。但在我们真实的应用程序中,我们使用Compose分页,它是在主组合内完成的。

你可以直接在你的viewmodel中保留与下拉刷新相关的状态和函数,比如refreshIndicatorStatelastRefreshTime。然而,在我的情况下,这相当啰嗨,因为我不得不在许多屏幕中实现相同的东西,所以我更喜欢创建一个可重用的组合并将相关数据包装在一个状态持有类中。

这是我们可重用的PullToRefreshLayout

@Composable
fun PullToRefreshLayout(
    modifier: Modifier = Modifier,
    pullRefreshLayoutState: PullToRefreshLayoutState,
    onRefresh: () -> Unit,
    content: @Composable () -> Unit,
) {
    val refreshIndicatorState by pullRefreshLayoutState.refreshIndicatorState
    val timeElapsedSinceLastRefresh by pullRefreshLayoutState.lastRefreshText

    val pullToRefreshState = rememberPullRefreshState(
        refreshing = refreshIndicatorState == RefreshIndicatorState.Refreshing,
        refreshThreshold = 120.dp,
        onRefresh = {
            onRefresh()
            pullRefreshLayoutState.refresh()
        },
    )

    LaunchedEffect(key1 = pullToRefreshState.progress) {
        when {
            pullToRefreshState.progress >= 1 -> {
                pullRefreshLayoutState.updateRefreshState(RefreshIndicatorState.ReachedThreshold)
            }

            pullToRefreshState.progress > 0 -> {
                pullRefreshLayoutState.updateRefreshState(RefreshIndicatorState.PullingDown)
            }
        }
    }

    Column(
        modifier = modifier
            .fillMaxSize()
            .pullRefresh(pullToRefreshState),
    ) {
        PullToRefreshIndicator(
            indicatorState = refreshIndicatorState,
            pullToRefreshProgress = pullToRefreshState.progress,
            timeElapsed = timeElapsedSinceLastRefresh,
        )
        Box(modifier = Modifier.weight(1f)) {
            content()
        }
    }
}

这是我们为此布局使用的状态持有类:

class PullToRefreshLayoutState(
    val onTimeUpdated: (Long) -> String,
) {

    private val _lastRefreshTime: MutableStateFlow<Long> = MutableStateFlow(System.currentTimeMillis())

    var refreshIndicatorState = mutableStateOf(RefreshIndicatorState.Default)
        private set

    var lastRefreshText = mutableStateOf("")
        private set

    fun updateRefreshState(refreshState: RefreshIndicatorState) {
        val now = System.currentTimeMillis()
        val timeElapsed = now - _lastRefreshTime.value
        lastRefreshText.value = onTimeUpdated(timeElapsed)
        refreshIndicatorState.value = refreshState
    }

    fun refresh() {
        _lastRefreshTime.value = System.currentTimeMillis()
        updateRefreshState(RefreshIndicatorState.Refreshing)
    }
}

@Composable
fun rememberPullToRefreshState(
    onTimeUpdated: (Long) -> String,
): PullToRefreshLayoutState =
    remember {
        PullToRefreshLayoutState(onTimeUpdated)
    }

通过上述所有步骤,当您想在一个屏幕上添加下拉刷新时,代码看起来像这样:

val pullToRefreshState = viewModel.pullToRefreshState

PullToRefreshLayout(
    modifier = Modifier.fillMaxSize(),
    pullRefreshLayoutState = pullToRefreshState,
    onRefresh = {
        viewModel.refresh()
    },
) {
    LazyColumn {}
}

额外功能 - 动画新项

如果在刷新列表时有新的项,您可以简单地通过为您的LazyLayout添加修饰符.animateItemPlacement()来使它们正确地进行动画。您还应该为您的项提供适当的ID,以使其正常工作。

额外功能 - 如果您有一个 UiState 呢?

如果您在同一个屏幕上还有一个带有加载、成功和错误状态的 uiState,那该怎么办呢?在这种情况下,刷新应该放在哪里?您可能会试图将刷新状态映射到 UiState.Loading,但您可能不希望在初始加载期间显示相同的刷新指示器。

如果您将刷新作为新的 UiState 添加进去呢?或者作为加载的子类型?您可能设法让它起作用,但请注意,如果您在这些状态之间切换您的组合,并且仅在成功情况下显示您的列表,那么在刷新期间您的列表将消失。这并不是我们在这种情况下想要的。我们希望列表保持在那里并向下移动。这就是为什么我更倾向于将刷新状态与我们已有的 ui state 分开。但是我仍然使用了一个变量来区分初始加载和刷新情况。这是因为数据层在获取开始时会发出一个加载状态,我不想将其映射到 UiState.Loading(以保持列表在那里)。

额外功能 - 使用 Compose Paging

如果您在同一个屏幕上还有Compose分页,那该怎么办呢?这就是我们的情况。所以您可能在 viewmodel 中有类似以下方式的分页流:

    val myItems = Pager(
            PagingConfig(pageSize = 20),
            pagingSourceFactory = {
                MyPagingSource(myUseCase)
            },
        ).flow.cachedIn(viewModelScope)

这是一个流,保存为一个变量。如何刷新它呢?我最初考虑将其作为一个返回 pager 流的函数,并在刷新时再次调用它,但是我会失去 cachedIn(viewModelScope) 部分,这对于保存分页状态和滚动位置非常重要。
我找到的解决办法是,将这个流从另一个在我想要刷新时改变的变量进行映射:

    private val _lastRefreshTime = pullToRefreshState.lastRefreshTime

    // 从 lastRefreshTime 进行映射的原因是为了强制执行刷新
    // 实际上,查询并不依赖于上次刷新时间。
    val myItems = _lastRefreshTime.flatMapLatest { _ ->
        Pager(
            PagingConfig(pageSize = 20),
            pagingSourceFactory = {
                MyPagingSource(myUseCase)
            },
        ).flow
    }.cachedIn(viewModelScope)

因此,当用户释放以进行刷新时,我们更新 lastRefreshTime,这个分页流就会被重新触发。(如果您的实现不关心上次刷新时间,您可以使用另一个变量。)

Github

https://github.com/OyaCanli/tutorial_samples/tree/master/PullRefreshComposeSample

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/138700.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【解刊】IEEE(trans),中科院2区,顶刊,CCF-A类,圈外人别想投?

计算机类 • 好刊解读 今天小编带来IEEE旗下计算机领域好刊的解读&#xff0c;如有相关领域作者有意向投稿&#xff0c;可作为重点关注&#xff01;后文有真实发表案例&#xff0c;供您投稿参考~ 01 期刊简介 IEEE Transactions on Computers ☑️出版社&#xff1a;IEEE …

PayPal的CISO谈GenAI如何提高网络安全

在最近一个季度(2023财年第二季度)&#xff0c;PayPal报告收入为73亿美元&#xff0c;同比增长7%&#xff0c;5%的交易增长和37%的增值服务收入增长带来了强劲的季度业绩。截至2022年&#xff0c;PayPal的营收为275亿美元。 在进入PayPal之前&#xff0c;Keren创建了两家网络安…

地面沉降监测站可以监测什么?

随着城市化的飞速发展&#xff0c;地面沉降问题日益凸显。为了及时掌握土地沉降情况&#xff0c;确保人们安全&#xff0c;就需要借助地面沉降监测站的力量。 一、实时监测土地沉降 地面沉降监测站的核心功能是实时监测土地沉降。通过高精度GNSS位移监测站和先进的数据分析技术…

CS224W6.3——图深度学习

在这篇中&#xff0c;将介绍图神经网络的架构。关键思想是&#xff0c;在GNNs中&#xff0c;根据局部网络邻域生成节点嵌入。gnn通常由任意数量的层组成&#xff0c;而不是单层&#xff0c;以集成来自更大上下文的信息。介绍了如何使用gnn来解决优化问题&#xff0c;以及它强大…

vue3配置@别名

在项目开发中&#xff0c;通常我们是不写相对路径的&#xff0c;因为有些文件需要在不同的文件中使用&#xff0c;如果使用相对路径&#xff0c;那么我们每次去CV路径的时候就要重新修改。因此通常我们是写跟路径的&#xff0c;但是从头开始又太过于麻烦&#xff0c;因此我们使…

行情不好,程序员的路在哪里?

最近有人提问&#xff0c;行情不好&#xff0c;程序员的路在哪里&#xff1f;今天的文章从远程工作、市场和流量思维、新技术、自媒体几个维度来讲讲。 远程工作 如果你在二三线城市&#xff0c;机会比较少&#xff0c;可以考虑一下远程工作。找一份美国或欧洲的远程工作&…

【OpenVINO】基于 OpenVINO C# API 部署 RT-DETR 模型

基于 OpenVINO C# API 部署 RT-DETR 模型 1. RT-DETR2. OpenVINO3. 环境配置4. 模型下载与转换5. C#代码实现5.1 模型推理类实现1. 模型推理类初始化2. 图片预测API 5.2 模型数据处理类RTDETRProcess1. 定义RTDETRProcess2. 输入数据处理方法3. 预测结果数据处理方法 6. 预测结…

【实例分割】用自己数据集复现经典论文YOLACT

YOLACT&#xff1a;You Only Look At CoefficienTs &#x1f3c6;论文下载&#xff1a;paper &#x1f3c6;代码下载&#xff1a;code &#x1f3c6;论文详解&#xff1a;YOLACT 目录 &#x1f342;&#x1f342;1.安装环境 &#x1f342;&#x1f342;2.数据准备 &…

AIGC|如何将Milvus集成到LangFlow中?详细代码演示!

目录 一、基本介绍 二、修改langflow代码使其支持milvus 三、效果演示 langflow是一个LangChain UI&#xff0c;它提供了一种交互界面来使用LangChain&#xff0c;通过简单的拖拽即可搭建自己的实验、原型流。通过在langflow中引入Milvus&#xff0c;用户可以更方便地存储和…

Mac电脑安装打印机驱动

1.在打印机背面找到型号&#xff0c;当想要安装的驱动在官网找不到时可直接搜索该系列&#xff1a;比如MF系列 2.安装完成后需要添加打印机 当打印机和电脑在同一个WiFi下的时候查找打印机IP&#xff0c;输入IP后可以查到对应的打印机&#xff0c;添加后即可使用

动态库和静态库

目录 一、动态库和静态库二、静态库2.1 生成静态库2.2 库搜索路径 三、动态库3.1 生成动态库3.2 使用动态库3.3 运行动态库3.4 使用外部库3.5 库文件名称和引入库的名称 四、动静态库及周边知识一览图 一、动态库和静态库 静态库&#xff08;.a&#xff09;&#xff1a;程序在…

如何获取1688商品详情,价格,图片

1688是阿里巴巴旗下的B2B电子商务平台&#xff0c;主要面向国内的生产商和批发商。 通过获取到的跨境属性数据&#xff0c;可以了解到商品的跨境属性&#xff0c;例如商品的语言、原产地、适用场景等信息。这些数据可以帮助用户更好地了解商品的特点和质量&#xff0c;做出更明…

频谱测量---测量信号的功率

频谱测量 通道功率、带宽、均值频率、中位数频率、谐波失真。 使用 obw 和 powerbw 查找信号的 90% 占用带宽和 3-dB 带宽。计算功率谱的均值或中位数频率。估计给定频带上的功率。测量谐波失真。估计瞬时带宽、瞬时频率、频谱熵和谱峭度。 函数 功率和带宽 bandpowerBand…

【避雷选刊】Springer旗下2/3区,2个月录用!发文量激增,还能投吗?

计算机类 • 好刊解读 前段时间小编分析过目前科睿唯安数据库仍有8本期刊处于On Hold状态&#xff0c;其中包括4本SCIE、4本ESCI期刊&#xff08;&#x1f449;详情可见&#xff1a;避雷&#xff01;又有2本期刊被标记“On Hold”&#xff01;含中科院2区&#xff08;TOP&…

研究前沿 | Science:单细胞测序助力绘制迄今最完善的灵长类动物前大脑发育图谱

引言 大脑发育的关键分子机制在啮齿动物中已有所了解&#xff0c;但在灵长类动物中仍然不清楚&#xff0c;这限制了研究者对高级认知能力起源和功能障碍的理解。此外&#xff0c;在包括人类在内的灵长类动物中&#xff0c;关于轴突投射路径上的丘脑区域和皮层区域多样化的早期分…

优思学院|如何利用六西格玛提升自己的大格局?

首先&#xff0c;我想说大格局并不仅仅是一个概念&#xff0c;更是一种生活态度。拥有大格局的人通常能够超越日常琐事&#xff0c;将目光投向更广阔的未来。他们不会被小事困扰&#xff0c;而是将注意力集中在更大的目标和使命上。拥有大格局的人常常具备卓越的领导力和判断力…

AIGC实战——自编码器(Autoencoder)

AIGC实战——自编码器 0. 前言1. 自编码器原理2. 数据集与模型分析2.1 Fashion-MNIST 数据集2.2 自编码器架构 3. 去噪自编码器3.1 编码器3.2 解码器3.3 连接编码器和解码器3.4 训练自编码器3.5 重建图像 4. 可视化潜空间5. 生成新图像小结系列链接 0. 前言 自编码器 (Autoenc…

Linux常用的解压命令

笑小枫的专属目录 整啥幺蛾子Linux tar命令tar命令的参数范例 Linux unzip命令语法参数 整啥幺蛾子 今天在linux解压一个文件&#xff0c;顺手就来tar -zxvf xxxx &#xff0c;哦吼&#xff0c;爆竿了&#xff0c;套他猴子的。 好吧&#xff0c;承认 .gz文件解压多了&#xff…

基于springboot实现结合疫情情况的婚恋系统【项目源码】计算机毕业设计

基于springboot实现结合疫情情况的婚恋系统演示 SpringBoot框架 SpringBoot是一个全新开源的轻量级框架。基于Spring4.0设计&#xff0c;其不仅继承了Spring框架原来有的优秀特性&#xff0c;而且还通过简化配置文件来进一步简化了Spring应用的整个搭建以及开发过程。另外在原…

电脑一直IP地址错误无法上网怎么办?

电脑出现IP地址错误&#xff0c;就将无法正常上网&#xff0c;那么&#xff0c;电脑一直IP地址错误无法上网怎么办呢&#xff1f;下面我们就一起来了解一下。 方法1. 确认是否禁用本地连接 你需要先确定是否禁用了本地网络连接&#xff0c;如果发现禁用&#xff0c;则将其启用…