在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 stateviewModel.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.refreshIndicatorStateval timeElapsedSinceLastRefresh by pullRefreshLayoutState.lastRefreshTextval 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 setvar lastRefreshText = mutableStateOf("")private setfun updateRefreshState(refreshState: RefreshIndicatorState) {val now = System.currentTimeMillis()val timeElapsed = now - _lastRefreshTime.valuelastRefreshText.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.pullToRefreshStatePullToRefreshLayout(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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Go语言]SSTI从0到1

[Go语言]SSTI从0到1 1.Go-web基础及示例2.参数处理3.模版引擎3.1 text/template3.2 SSTI 4.[LineCTF2022]gotm1.题目源码2.WP 1.Go-web基础及示例 package main import ("fmt""net/http" ) func sayHello(w http.ResponseWriter, r *http.Request) { // 定…

微服务 Spring Cloud 6,用了这么多年Docker容器,殊不知你还有这么多弯弯绕

目录 一、神之容器 Docker二、Docker架构图1、Docker Client 客户端2、Docker Daemon 守护进程3、镜像&#xff08;Image&#xff09;4、Docker Driver 驱动模块5、Docker Graph内部数据库6、Docker Libcontainer函数库7、Docker Container 容器实例 三、Docker安装1、卸载Dock…

Sui学术研究奖公布,资助研究者探索人工智能、能源市场和区块链游戏

Sui基金会高兴地宣布首轮Sui学术研究奖&#xff08;SARAs&#xff09;的获奖者。SARAs计划提供资助&#xff0c;支持推动Sui区块链技术的研究。学术和研究界对我们的初次征集呈现出大量高质量的提案。 已接受的九个提案涵盖了各种主题&#xff0c;如token经济学、智能合约机制…

微信个人号二次开发之检测好友

简要描述&#xff1a; 检测好友状态 请求URL&#xff1a; http://域名地址/userPrivacySettings 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 参数&#xff1a; 参数名必选…

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

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

跨境电商商城源码:实现多语言、多货币、多商户入驻的全面解决方案

随着全球电子商务的迅猛发展&#xff0c;越来越多的商家和消费者选择在跨境电商平台上进行交易。为了满足不同国家和地区的需求&#xff0c;多语言、多货币、多商户入驻已成为跨境电商平台的核心竞争力。本文将为您介绍如何通过跨境电商商城源码实现这些功能&#xff0c;帮助您…

人工智能与发电玻璃:未来能源技术的融合

人工智能与发电玻璃&#xff1a;未来能源技术的融合 摘要&#xff1a;本文探讨人工智能与发电玻璃这两项技术的结合&#xff0c;共同推动能源领域的创新。本文将介绍发电玻璃工作原理及应用、人工智能在发电玻璃的应用领域以及共同为可持续能源发展做出贡献。 一、引言 随着科…

一款好用的jpeg分析软件 JPEGsnoop

最近解码器解码jpeg的时候出了问题&#xff0c;为了追踪问题&#xff0c;找到了这款免费好用的jpeg分析软件- JPEGsnoop。 顶礼膜拜。 贴上链接地址&#xff1a; https://github.com/ImpulseAdventure/JPEGsnoop/releases 上面已经有编译好的win10 exe了 下载后解压&#x…

亚马逊鲲鹏系统强大的指纹系统可有效防止账号关联

亚马逊鲲鹏系统最新的防指纹技术支持绑定不同的代理IP&#xff0c;可以根据ip创建不同的指纹环境&#xff0c;让账号伪装成来自不同地点、不同设备的流量&#xff0c;每个账号环境隔离开来&#xff0c;实现了完全独立的操作任务&#xff0c;避免了账户指纹关联和操作轨迹关联。…

基于springboot实现生鲜超市管理的设计与实现系统【项目源码】

基于springboot实现生鲜超市管理的设计与实现系统演示 Java技术 Java是由Sun公司推出的一门跨平台的面向对象的程序设计语言。因为Java 技术具有卓越的通用性、高效性、健壮的安全性和平台移植性的特点&#xff0c;而且Java是开源的&#xff0c;拥有全世界最大的开发者专业社群…

回顾 — SFA:简化快速 AlexNet(模糊分类)

模糊图像的样本 一、说明 在本文回顾了基于深度学习的模糊图像分类&#xff08;SFA&#xff09;。在本文中&#xff1a;Simplified-Fast-AlexNet (SFA)旨在对图像是否因散焦模糊、高斯模糊、雾霾模糊或运动模糊而模糊进行分类。 二、大纲 图像模糊建模简要概述简化快速 AlexNet…

【Git】Git的GUI图形化工具ssh协议IDEA集成Git

一、GIT的GUI图形化工具 1、介绍 Git自带的GUI工具&#xff0c;主界面中各个按钮的意思基本与界面文字一致&#xff0c;与git的命令差别不大。在了解自己所做的操作情况下&#xff0c;各个功能点开看下就知道是怎么操作的。即使不了解&#xff0c;只要不做push操作&#xff0c;…

测量均值频率、功率、带宽

测量均值频率、功率、带宽 生成以 1024 kHz 采样的啁啾信号的 1024 个采样点。啁啾信号的初始频率为 50 kHz&#xff0c;采样结束时达到 100 kHz。添加高斯白噪声&#xff0c;使信噪比为 40 dB。 nSamp 1024; Fs 1024e3; SNR 40;t (0:nSamp-1)/Fs;x chirp(t,50e3,nSamp/…

图像实时采集系统

本方案主要在于解决图像实时采集系统对算法校正的仿真实验&#xff0c;以及采集卡接收电路的验证。 由于图像实时跟踪处理系统需要大量的外场景实验&#xff0c;大部分时候只能通过采集的现场图像以在电脑软件中读取图片的形式来进行验证算法&#xff0c;而无法通过采集卡对接…

DMP大湾区工博会开幕在即,狂撒100万福利,邀您与2200+展商面对面

截止11月9日&#xff0c;DMP大湾区工博会2023已迎来超100万人关注。大湾区工博会将于11月27-30日在深圳国际会展中心(宝安)举办。作为工业制造行业的风向标&#xff0c;展会将带来2200多家全球参展企业、40多场主题演讲、数千项行业新品技术。 本届DMP大湾区工博会&#xff0c;…

学习c#的第九天

C# 可空类型&#xff08;Nullable&#xff09; C# 可空类型&#xff08;Nullable&#xff09; 可空类型允许我们在值类型中包含 null 值&#xff0c;这在处理数据库查询结果或需要表示缺失值的情况时非常有用。 声明一个可空类型的语法如下&#xff1a; < data_type>…

【rl-agents代码学习】01——总体框架

文章目录 rl-agent Get startInstallationUsageMonitoring 具体代码 学习一下rl-agents的项目结构以及代码实现思路。 source: https://github.com/eleurent/rl-agents rl-agent Get start Installation pip install --user githttps://github.com/eleurent/rl-agentsUsage…

大数据-之LibrA数据库系统告警处理(ALM-12041 关键文件权限异常)

告警解释 系统每隔一个小时检查一次系统中关键目录或者文件权限、用户、用户组是否正常&#xff0c;如果不正常&#xff0c;则上报故障告警。 当检查到权限等均正常&#xff0c;则告警恢复。 告警属性 告警ID 告警级别 可自动清除 12041 严重 是 告警参数 参数名称 …

​ArcGIS Pro怎么生成山顶点

山顶点是指山脉、山丘或山脉系统中最高的地点&#xff0c;通常是山的最高峰&#xff0c;这是山地地貌中的最高点&#xff0c;往往是山脉的标志性特征之一&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro获取山顶点&#xff0c;希望能对你有所帮助。 数据来源 本教程所使用…

大洋钻探系列之二IODP 342航次是干什么的?(上)

本文简单介绍一下大洋钻探IODP 342航次&#xff0c;从中&#xff0c;我们一窥大洋钻探航次的风采。 IODP342的航次报告在网络上可以下载&#xff0c;英文名字叫《Integrated Ocean Drilling ProgramExpedition 342 Preliminary Report》&#xff0c;航次研究的主要内容是纽芬兰…