Jetpack Compose:使用PagerIndicator和Infinity实现滚动的HorizontalPager
Jetpack Compose:使用PagerIndicator和Infinity实现滚动的HorizontalPager

可能你已经知道,Jetpack Compose 默认不包含内置的ViewPager组件。然而,我们可以通过在 build.gradle 文件中添加 accompanist 库依赖,将 ViewPager 功能集成到我们的项目中。
implementation "com.google.accompanist:accompanist-pager:0.28.0"
为了将指示器纳入其中,我们还将利用accompanist库。
implementation "com.google.accompanist:accompanist-pager-indicators:0.28.0"
注意:对于此项目,我们使用的是Compose版本1.3.1和Kotlin版本1.8.10。
让我们从创建一个HorizontalPager 开始
accompanist库的存在,创建HorizontalPager是一项简单的任务。
HorizontalPager(count = pageCount,state = pagerState,modifier = modifier
) {// page content
}
- Count:页面数
我们将计数设置为非常大的数,如Int.MAX_VALUE,这样我们就可以实现无限滚动行为。
// Used Int.MAX_VALUE for infinity scroll
val pageCount = Int.MAX_VALUE
- State:用于控制或观察分页器状态的状态对象。
对于状态创建,我们只需要 initialPage 值。为了实现双向无限滚动,我们应该从给定页面计数的中间开始。因此,initialPage 可以设置如下示例:
val middlePage = pageCount / 2
val pagerState = rememberPagerState(initialPage = middlePage)
尽管一开始所有东西似乎都是正确的,但我们很快就会遇到一个涉及最初显示页面的问题。
技巧
为了实现ViewPager的无限行为,我们通过设置计数为一个非常大的数字来实现了一种解决方案。但是,我们实际的物品列表(奖杯)要小得多。为了确保ViewPager显示我们真正列表中的正确页面而不创建重复页面,我们需要适当地处理页面编号。
为了解决这个挑战,我们将提供的页面编号除以奖杯列表的大小。这个除法允许我们在我们的真实列表中获取正确的页面索引。通过执行这个计算,我们确保ViewPager只显示列表中的实际物品,防止任何重复。
通过利用这种方法,我们可以在ViewPager中轻松导航通过奖杯列表,同时保持其无限行为。
看下面的例子:
val realSize = trophies.sizeHorizontalPager(count = pageCount,state = pagerState,modifier = modifier
) { page -> val realPage = page % realSize// max value is trophies.sizeTrophyWidget(realPage, trophy = trophies[realPage])
}
你懂了吗?不懂?(那我们就来算一下吧!)

看起来我们的数学运算正常运行!
如果您仔细观察,就会发现初始页面不是奖杯列表中的第一个。实际上,初始状态取决于奖杯列表的大小。为了解决这个差异并确保正确的初始状态,有必要计算并传递一个参数到ViewPager状态。
val realSize = trophies.sizeval middlePage = pageCount / 2
// Init the PagerState with a very large number and make it always start from the first item of the real list
val pagerState = rememberPagerState(initialPage = middlePage - (middlePage % realSize))
通过将middlePage减去middlePage与奖杯数量取余的结果,确保ViewPager将从奖杯列表的开头开始。
页面指示器
添加指示器也很简单,我们只需要添加HorizontalPagerIndicator并将pagerState作为参数传递即可。

然而,这里存在一个问题!如果您尝试在不指定列表(奖杯)的实际大小的情况下使用pagerState,则应用程序将会空白页。那是因为HorizontalPagerIndicator的默认pageCount设置为PagerState.pageCount的值,而在我们的情况下,这是一个非常大的数。
幸运的是,我们可以通过将pageCount作为参数添加到HorizontalPagerIndicator中来指定pageCount。
看一个例子:
HorizontalPagerIndicator(pagerState = pagerState,pageCount = realSize,pageIndexMapping = { it % realSize },activeColor = Color.White,modifier = modifier.align(Alignment.BottomCenter).padding(bottom = 12.dp)
)
我们还必须描述如何通过将页面传递给pageIndexMapping函数来获取活动指示器的位置。这可以通过将pagerState.currentPage除以奖杯列表的大小来实现。
如上例所示,您可以实现以下代码段:
pageIndexMapping = { currentPage % realSize }
要获取活动指示器的位置,您可以使用pageIndexMapping函数,并使用pagerState.currentPage和奖杯列表的大小执行模运算。
自动滚动
如果您还需要您的页面自动滚动,可以使用以下代码片段:
// Start auto-scroll effect
LaunchedEffect(isDraggedState) {// convert compose state into flowsnapshotFlow { isDraggedState.value }.collectLatest { isDragged ->// if not isDragged start slide animationif (!isDragged) {// infinity loopwhile (true) {// duration before each scroll animationdelay(5_000L)runCatching {pagerState.animateScrollToPage(pagerState.currentPage.inc() % pagerState.pageCount)}}}}
}
完整代码如下:
private const val SCROLL_ANIMATION_DURATION = 5_000L@OptIn(ExperimentalPagerApi::class)
@Composable
fun InfinityHorizontalPager(modifier: Modifier = Modifier) {Column(verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Box(modifier = modifier.fillMaxWidth().height(400.dp)) {// Used Int.MAX_VALUE for infinity scrollval pageCount = Int.MAX_VALUE// The actual view pager size (for the HorizontalPagerIndicator)val realSize = trophies.size// Start from the middle in order to the infinity scroll for both sidesval middlePage = pageCount / 2// Init the PagerState with a very large number and make it always start from the first item of the real listval pagerState = rememberPagerState(initialPage = middlePage - (middlePage % realSize))val isDraggedState = pagerState.interactionSource.collectIsDraggedAsState()HorizontalPager(count = pageCount,state = pagerState,modifier = modifier.fillMaxWidth().fillMaxHeight().background(MaterialTheme.colors.background),) {val page = it % realSize// max value is trophies.sizeTrophyWidget(page, trophy = trophies[page])}Surface(modifier = Modifier.padding(bottom = 8.dp).align(Alignment.BottomCenter),shape = CircleShape,color = Color.Black.copy(alpha = 0.5f)) {HorizontalPagerIndicator(pagerState = pagerState,pageCount = realSize,pageIndexMapping = { it % realSize },activeColor = Color.White,modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp))}// Start auto-scroll effectLaunchedEffect(isDraggedState) {// convert compose state into flowsnapshotFlow { isDraggedState.value }.collectLatest { isDragged ->// if not isDragged start slide animationif (!isDragged) {// infinity loopwhile (true) {// duration before each scroll animationdelay(SCROLL_ANIMATION_DURATION)runCatching {pagerState.animateScrollToPage(pagerState.currentPage.inc() % pagerState.pageCount)}}}}}}}
}@Composable
fun TrophyWidget(page: Int,trophy: TrophyCard,modifier: Modifier = Modifier
) {Box(modifier = modifier.padding(horizontal = 16.dp).fillMaxWidth().fillMaxHeight().background(Color.Black).clip(shape = RoundedCornerShape(size = 12.dp)),) {AsyncImage(model = ImageRequest.Builder(LocalContext.current).data(trophy.image).crossfade(true).build(),modifier = modifier.fillMaxWidth().fillMaxHeight().clip(shape = RoundedCornerShape(size = 12.dp)),contentDescription = null,contentScale = ContentScale.FillBounds)Column(modifier = Modifier.fillMaxWidth().background(color = Color.Black.copy(alpha = 0.5f)).padding(10.dp).align(Alignment.BottomStart)) {Text(text = trophy.location,color = Color.White,style = Typography.h6,textAlign = TextAlign.Center)Text(text = trophy.year,color = Color.White,style = Typography.h4,textAlign = TextAlign.Center)}Text(text = "$page",style = Typography.body1,color = Color.Black,textAlign = TextAlign.Center,modifier = Modifier.padding(10.dp).clip(shape = RoundedCornerShape(size = 4.dp)).background(Color.White).padding(10.dp).align(Alignment.BottomEnd))}
}@Preview(showBackground = true, showSystemUi = true)
@Composable
fun DefaultPreview() {InfinityPagerTheme {Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colors.background) {InfinityHorizontalPager()}}
}

相关文章:
Jetpack Compose:使用PagerIndicator和Infinity实现滚动的HorizontalPager
Jetpack Compose:使用PagerIndicator和Infinity实现滚动的HorizontalPager 可能你已经知道,Jetpack Compose 默认不包含内置的ViewPager组件。然而,我们可以通过在 build.gradle 文件中添加 accompanist 库依赖,将 ViewPager 功能…...
2023年杭州/广州/东莞/深圳软考(中/高级)认证,618报名特惠
软考是全国计算机技术与软件专业技术资格(水平)考试(简称软考)项目,是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试,既属于国家职业资格考试,又是职称资格考试。 系统集成…...
springboot项目外卖管理 day03-公共字段填充与新增删除分类
文章目录 一、公共字段自动填充1.1、问题分析1.2、代码实现1.2.1、在实体类的属性上加入TableField注解,指定自动填充的策略1.2.2按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口1.2.3 功能…...
Nginx:Tomcat部署及优化(一)
Nginx:Rewrite 一、Tomcat介绍1.1 Tomcat 简介1.2 Tomcat 核心的组件1.2.1 什么是 servlet1.2.2 什么是 JSP 1.3 Tomcat 功能组件结构1.3.1 Container 结构分析 1.4 Tomcat 请求过程 二、Tomcat 服务部署三、Tomcat 虚拟主机配置四、Tomcat多实例部署 一、Tomcat介绍…...
Docker Swarm 集群搭建和使用 —— 筑梦之路
简单介绍 swarm 集群由管理节点(Manager)和工作节点(Worker)构成。 管理节点:主要负责整个集群的管理工作包括集群配置、服务管理等所有跟集群有关的工作。诸如监控集群状态、分发任务至工作节点等操作。 工作节点:主要负责执行运行服务的任务。 官方文档:docker swarm…...
是否需要更换CRM系统如何评估?如何确保更换成功?
很多企业在使用CRM客户管理系统的过程中,并没有达到预期的效果,甚至出现了实施失败的情况。部分企业可能会考虑更换CRM系统,以期获得更好的结果。但是,更换CRM系统是否值得呢?下面我们就来说说。 一、是否该更换CRM …...
CSDN竞赛57期题解
总结 交卷时一看才六十多分还有点吃惊,一看非编程题部分还是丢了二十分。填空题是这类竞赛最大的诟病,答案是名词的必然不唯一,答案需要计算的给定的参考答案必然计算错误,更离谱的是题目出成这样,反馈后官方竟然一点…...
springboot+vue.js大学生竞赛报名作品评分管理系统
本文介绍了大学生竞赛管理系统的开发全过程。通过分析大学生竞赛管理系统管理的不足,创建了一个计算机管理大学生竞赛管理系统的方案。文章介绍了大学生竞赛管理系统的系统分析部分,包括可行性分析等,系统设计部分主要介绍了系统功能设计和数…...
Python爱好者的自我修养(1):简单输入与输出
Python简单输入与输出 1.输出1.1 简单输出1.2 转义字符1.2.1 定义1.2.2 常见的转义字符用法 2.输入3.温馨提示 终于…… 终于…… 我开始玩Python了 (不是C不学了哈,C还是照更~) 今天先来简单讲下输入和输出 1.输出 1.1 简单输出 输出的函…...
java SSM 摄影作品网站myeclipse开发mysql数据库springMVC模式java编程计算机网页设计
一、源码特点 java SSM 摄影作品网站系统是一套完善的web设计系统(系统采用SSM框架进行设计开发,springspringMVCmybatis),对理解JSP java编程开发语言有帮助,系统具有完整的源代 码和数据库,系统主要采…...
[Maven高级]->近万字文章带你深入了解Maven
⭐作者介绍:大二本科网络工程专业在读,持续学习Java,努力输出优质文章 ⭐作者主页:逐梦苍穹 ⭐所属专栏:JavaEE ⭐如果觉得文章写的不错,欢迎点个关注一键三连😉有写的不好的地方也欢迎指正&…...
物联网Lora模块从入门到精通(五)光照与温湿度传感器
一、前言 在程序开发中,光照与温湿度的获取是十分常见与重要的,本文我们主要是使用M21温湿度光照三合一传感器,其中温湿度数据通过协议获取,而光照通过ADC获取。 二、代码实现 本文内容较为简单,且后续文章将在本文基…...
【网络编程】计算机网络基础知识总结 | 运输层 |TCP协议
文章目录 前言一、计算机网络层次结构二、网络层三、运输层3.1、TCP/IP协议介绍3.2、端口(协议端口号)3.3、套接字3.4、TCP实现原理3.4.1、TCP的特点3.4.2、停止等待协议3.4.3、滑动窗口协议3.4.4、拥塞控制3.4.5、TCP连接的三个阶段 3.5、UDP实现原理 前…...
python关键知识点
1. 变量:在程序中存储值或对象的名称。 2. 数据类型:指变量的数据类型,例如 str、int、float、list、tuple、dict、set 等。 3. 操作符:表示运算符号,例如加号 和减号 -。 4. 循环:通过重复执行某个代码…...
c# 从零到精通 数组的操作-将两个一维数组合并成一个二维数组
c# 从零到精通 数组的操作-将两个一维数组合并成一个二维数组 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Test07 { class Program { static void Main(string[] args) { //定义两个一维数组 int[] arr1 new int[] {…...
Linux目录结构(与window目录结构对比+绝对路径和相对路径)
一、Linux目录结构 Linux目录结构是一个标准化的文件系统层次结构,非常有组织性并且易于管理。而与Windows 操作系统不同,Linux将所有文件和设备都组织在一个单一的根目录下。以下是Linux的标准目录结构: /:根目录,包含…...
投票活动小程序开发搭建
由于小程序是基于微信开发者工具编写的,因此我先介绍一下需要使用的工具和技术: - 微信开发者工具:用于开发、调试和发布小程序。 - 小程序云开发:用于存储数据和进行后端逻辑处理。 - uni-app框架:uni-app 是一个使…...
代码随想录day18
513.找树左下角的值 本题用前中后序都可以(都是先遍历左再遍历右,保证最后一定是左侧的节点),因为没有中节点的处理逻辑,用全局变量记录最大深度,只要遇到叶子结点并且当前深度比最大深度大,就更…...
QT+OpenGL高级光照 Blinn-Phong和Gamma校正
QTOpenGL高级光照1 本篇完整工程见gitee:QtOpenGL 对应点的tag,由turbolove提供技术支持,您可以关注博主或者私信博主 Blinn-Phong 冯氏光照:视线与反射方向之间的夹角不小于90度,镜面光分量会变成0.0(不是很合理&am…...
【Ubuntu系统内核更新与卸载】
【Ubuntu系统内核更新与卸载】 1. 前言2. 内核安装2.1 系统更新2.2 官网下载 3. 内核卸载3.1 需求分析3.2 卸载方法 1. 前言 我们在搭建环境时常常遇到内核版本不匹配的问题,需要我们安装新的内核版本;有时又会遇到在安装软件时报错boot空间已满无法安装…...
为什么你需要m4s-converter:让B站缓存视频重获自由的秘密武器
为什么你需要m4s-converter:让B站缓存视频重获自由的秘密武器 【免费下载链接】m4s-converter 一个跨平台小工具,将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你是否曾经遇到过这样的…...
ARM GICv5 IRS寄存器架构与缓存控制机制详解
1. ARM GICv5 IRS寄存器架构解析中断控制器(GIC)是现代SoC设计中不可或缺的核心组件,负责高效管理和分发系统中各类中断请求。GICv5版本引入的中断路由服务(IRS)模块代表了ARM架构在中断处理领域的重大革新。IRS通过精心设计的寄存器组实现了前所未有的中断管理灵活…...
嵌入式开发十年痛点解析:技术栈、多核与安全的实战解法
1. 从一场会议邀约说起:嵌入式程序员的“午夜惊魂”前几天整理旧资料,翻到了2014年嵌入式系统大会(ESC)编程专题的公开征集帖,发起人是当时ARM的培训经理Chris Shore。帖子标题很有意思,叫“什么让你夜不能…...
Exclusively Dark数据集:破解低光照视觉难题的7363张真实图像基准
Exclusively Dark数据集:破解低光照视觉难题的7363张真实图像基准 【免费下载链接】Exclusively-Dark-Image-Dataset Exclusively Dark (ExDARK) dataset which to the best of our knowledge, is the largest collection of low-light images taken in very low-li…...
3步搭建你的英雄联盟智能助手:LeagueAkari完整操作指南
3步搭建你的英雄联盟智能助手:LeagueAkari完整操作指南 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit 想象一下,当你正…...
用Wireshark抓包实战解析USB控制传输:从SETUP包到ACK的完整流程
用Wireshark实战拆解USB控制传输:从设备枚举到数据交互的深度解析 当你第一次插入USB设备时,主机和设备之间究竟发生了什么?那些看似神秘的SETUP令牌包、DATA0数据包背后隐藏着怎样的通信逻辑?本文将带你用Wireshark这个"网络…...
别再只调参了!搞懂MaxPool2D的padding=‘same‘和‘valid‘,让你的CNN模型效果立竿见影
别再只调参了!搞懂MaxPool2D的paddingsame和valid,让你的CNN模型效果立竿见影 在构建卷积神经网络(CNN)时,许多开发者习惯性地将注意力集中在卷积核大小、激活函数选择等显性参数上,却常常忽略池化层中padd…...
可口可乐AI印相私密工作流首次公开(含内部CMYK预置包、罐体反光建模提示词库与印刷出血校准表)
更多请点击: https://intelliparadigm.com 第一章:可口可乐AI印相私密工作流的起源与战略价值 可口可乐AI印相私密工作流并非源于通用大模型的简单套用,而是其全球数字创新实验室在2022年启动的“Project Chroma”中孵化出的端到端隐私增强…...
从源码细节看muduo为何比libevent2快70%:一次4096字节读取限制引发的性能思考
从缓冲区设计揭秘高性能网络库的优化哲学 在构建高并发服务器时,网络库的性能差异往往源于看似微小的设计决策。当两个知名网络库在相同硬件条件下出现70%的吞吐量差距时,这个数字背后隐藏的是对系统调用、内存管理和数据流控制的深刻理解差异。本文将从…...
MySQL 基础:SELECT、WHERE、JOIN 的第一次使用
不用怕数据库,跟着这三个单词,你就能查到你想要的一切。欢迎来到 《大一突围》 专栏。很多大一同学第一次接触 MySQL,看到“数据库”三个字就觉得很难。其实,你日常生活中每天都在“查数据”——查成绩、翻通讯录、筛选淘宝商品……...

