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

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()
        }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Calvin880828

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值