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

news2024/11/27 13:44:03

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

Android | Kotlin | Jetpack Compose | ViewPager | PagerIndicator
可能你已经知道,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.size

HorizontalPager(
    count = pageCount,
    state = pagerState,
    modifier = modifier
) { page -> 
    val realPage = page % realSize
    // max value is trophies.size
    TrophyWidget(realPage, trophy = trophies[realPage])
}

你懂了吗?不懂?(那我们就来算一下吧!)
page count

看起来我们的数学运算正常运行!

如果您仔细观察,就会发现初始页面不是奖杯列表中的第一个。实际上,初始状态取决于奖杯列表的大小。为了解决这个差异并确保正确的初始状态,有必要计算并传递一个参数到ViewPager状态。

val realSize = trophies.size

val 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作为参数传递即可。

Android | HorizontalPager | Indicator
然而,这里存在一个问题!如果您尝试在不指定列表(奖杯)的实际大小的情况下使用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 flow
    snapshotFlow { isDraggedState.value }
        .collectLatest { isDragged ->
            // if not isDragged start slide animation
            if (!isDragged) {
                // infinity loop
                while (true) {
                    // duration before each scroll animation
                    delay(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 scroll
            val 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 sides
            val 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))
            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.size
                TrophyWidget(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 effect
            LaunchedEffect(isDraggedState) {
                // convert compose state into flow
                snapshotFlow { isDraggedState.value }
                    .collectLatest { isDragged ->
                        // if not isDragged start slide animation
                        if (!isDragged) {
                            // infinity loop
                            while (true) {
                                // duration before each scroll animation
                                delay(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()
        }
    }
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

有了这套方案,企业降本增效不再是纸上谈兵 (2)

一、生存压力逼近,企业如何应对经济下行残酷挑战? 当前市场经济下滑,客户预算紧缩和投资削减可能导致IT项目推迟或取消,从而直接影响公司收入和盈利能力。各大厂商都在陆续裁员或调整业务,以人力等成本为主的IT公司也必…

双塔模型dssm实践

最近在学习向量召回,向量召回不得不用到dssm双塔模型,双塔模型的原理非常简单,就是用两个任务塔,一个是query侧的query任务塔,另一个是doc侧的doc任务塔,任务塔向上抽象形成verctor隐向量后,用c…

【多同步挤压变换】基于多同步挤压变换处理时变信号和噪声信号研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

2022年国赛高教杯数学建模B题无人机遂行编队飞行中的纯方位无源定位解题全过程文档及程序

2022年国赛高教杯数学建模 B题 无人机遂行编队飞行中的纯方位无源定位 原题再现 无人机集群在遂行编队飞行时,为避免外界干扰,应尽可能保持电磁静默,少向外发射电磁波信号。为保持编队队形,拟采用纯方位无源定位的方法调整无人机…

win10录屏软件哪个好用?强烈推荐这3款!

案例:想要录制我的电脑屏幕,但是不知道如何选择合适的录屏工具,有没有好用的win10录屏软件? 【我想找一款好用的win10录屏工具,录制我的电脑屏幕,但是找了很久还没有找到,大家有好用的录屏软件…

Kibana:使用 Kibana 自带数据进行可视化(二)

在今天的练习中,我们将使用 Kibana 自带的数据来进行一些可视化的展示。希望对刚开始使用 Kibana 的用户有所帮助。这个是继上一篇文章 “Kibana:使用 Kibana 自带数据进行可视化(一)” 的续篇。 前提条件 如果你还没有安装好自己…

占据80%中国企业出海市场,亚马逊云科技如何为出海客户提供更多资源和附加值

亚马逊云科技就可以做到,作为占据80%中国企业出海市场的亚马逊云科技,其覆盖全球的业务体系,从亚马逊海外购、亚马逊全球开店、亚马逊智能硬件与服务,Amazon Alexa,Amazon Music都是属于亚马逊云科技“梦之队”的一员。…

【Android】WMS(二)Window的添加

软件盘相关模式 在 Android 应用开发中,软键盘的显示与隐藏是一个经常出现的问题,而 WindowManager 的 LayoutParams 中定义的软键盘相关模式则为开发者提供了一些解决方案。 其中,SoftInputMode 就是用于描述软键盘的显示方式和窗口的调整…

【LeetCode】HOT 100(6)

题单介绍: 精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。 目录 题单介绍&#…

python包装与授权

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和…

ai聊天对话工具哪种好用?这些ai对话聊天工具不要错过

在如今信息爆炸的时代,人工智能技术正在逐渐渗透到我们的生活和工作中。ai对话聊天技术作为其中的一项重要应用,吸引了越来越多的关注。但是,ai对话聊天技术并不是万能的,它需要一定的技巧和策略才能真正发挥其价值。那么&#xf…

CAN总线转串口

一、CAN总线在工程机械中的广泛应用 随着科技的进步和现代施工项目大型化的要求,新一代工程机械需要实现集成化操作和智能控制。CAN总线是国际上应用最广泛的现场总线之一。CAN总线以其高可靠性、实时性、无破坏仲裁、多主等特性,已广泛应用于工程机械中&#xff0c…

这里推荐几个前端动画效果网站

1. AnimistaAnimista 是一个 CSS 动画/转场库和在线工具。它有许多现成的 CSS 动画片段可以直接使用,也可以在线定制动画。 网站地址:Animista - On-Demand CSS Animations Library 2. Animate.cssAnimate.css 是一个免费的 CSS 动画库,里面有 Attention Seekers 、 Bouncing E…

【Java|多线程与高并发】线程安全问题以及synchronized使用实例

文章目录 1. 前言2. 线程安全问题演示3.线程安全问题的原因4.synchronized关键字5. 总结 1. 前言 Java多线程环境下,多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。 线程安全一直都是一个令人头疼的问题.为了解决这个问题,Java为我们提供了很多方式…

MySQL为什么有了redolog还需要double write buffer?

MySQL为什么有了redolog还需要double write buffer? 问题 我们知道MySQL InnoDB引擎使用redolog作为异常容灾恢复的机制,当MySQL进程发生异常退出、机器断电等,在重新启动时,使用redolog恢复。 OK,redolog是被MySQL…

进程同步与进程通信(#include <windows.h>)

目录 实验二 进程同步与进程通信 一、实验目的 二、实验内容 任务一、进程同步与互斥 任务二、进程通信 实验二 进程同步与进程通信 备注:大二(下)操作系统实验二 一、实验目的 掌握基本的同步与互斥算法,理解P&#xff…

移植蓝牙芯片后,PCM 无声音问题记录

背景:投影仪项目上的蓝牙模组本地已经验证ok,送到客户那里发现HFP打电话没声音。 1. 客户平台是3566,android 11的环境, 该环境下其他的模组是可以的 2. 在3566上安装QQ, 波通VOIP电话后, 无阴影, 3. 通过示波器接收pcm 无波形输出, 问题分析查证 1.查看HCI log ,…

【LeetCode热题100】打卡第17天:接雨水全排列旋转图像

文章目录 【LeetCode热题100】打卡第17天:接雨水&全排列&旋转图像⛅前言 接雨水🔒题目🔑题解 全排列🔒题目🔑题解 旋转图像🔒题目🔑题解 【LeetCode热题100】打卡第17天:接雨…

Elasticsearch 中文分词器

IK 分词器 我们在ES中最常用的中文分词器就是IK分词器,其项目地址为:https://github.com/medcl/elasticsearch-analysis-ik 下载安装 下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases 下载时注意和es的版本对应&a…

Network 之十二 iPXE 源码、编译过程、Linker tables 机制、移植新驱动、固件使用

最近,正在学习 iPXE 源码,于是开始各种 Google 查找 iPXE 的资料进行学习。以下就是学习过程中一些感觉比较重要的点,特此记录,以备后续查阅。 起源 上世纪 90 年代初,网卡开始在其扩展卡上包含启动 ROM,每…