こんにちは、 id:numanuma08 です。Jetpack Composeで無限にスクロールするPagaerを作ります。
こういうやつです。
表示したいコンテンツの数に関わらずPagerのページ数をInt.MAX_VALUE
とし、初期位置をInt.MAX_VALUE / 2
とします。表示するコンテンツのindexはHorizontalViewPager
のパラメータに取る無名関数のパラメーターで与えられるindexを使って実際のコンテンツのindexを計算して取得します。
val initialIndex = Int.MAX_VALUE / 2 val pagerState = rememberPagerState( initialPage = initialIndex, pageCount = { Int.MAX_VALUE }, ) // 表示するコンテンツ。Compose関数のパラメータなどで渡されることを想定 val itemCount = 3 HorizontalPager( state = pagerState, pageSpacing = 16.dp, pageSize = PageSize.Fixed(200.dp), // 常に左右に隣のコンテンツが見切れているようにする snapPosition = SnapPosition.Center, ) { page -> val index = page % itemCount Box( modifier = Modifier .fillMaxWidth() .height(200.dp) .background(Color.Red), contentAlignment = Alignment.Center, ) { Text("Page: $index") } }
いちおう理論上Int.MAX_VALUE / 2
回同じ方向にスクロールされるとページの最後に到達してしまうので対策をしておきます。
val scope = rememberCoroutineScope() // Int.MAX_VALUE / 2 回スクロールされてもスクロールが終了しないようにするため // スクロールごとにページ位置を Int.MAX_VALUE / 2 付近に戻す // ユーザーのスクロール中にLaunchedEffectが実行されるため、別のコルーチンスコープを実行して、 // スクロールが中途半端にcancelされないようにする LaunchedEffect(key1 = pagerState.currentPage) { scope.launch { if (!pagerState.isScrollInProgress) { pagerState.animateScrollToPage( page = (Int.MAX_VALUE / 2) + pagerState.currentPage % itemCount, ) } } }
以上を踏まえたComposable関数の全体が以下です。
@Composable fun InfinityPager(modifier: Modifier = Modifier) { val initialIndex = Int.MAX_VALUE / 2 val pagerState = rememberPagerState( initialPage = initialIndex, pageCount = { Int.MAX_VALUE }, ) val itemCount = 3 val scope = rememberCoroutineScope() // Int.MAX_VALUE / 2 回スクロールされてもスクロールが終了しないようにするため // スクロールごとにページ位置を Int.MAX_VALUE / 2 付近に戻す // ユーザーのスクロール中にLaunchedEffectが実行されるため、別のコルーチンスコープを実行して、 // スクロールが中途半端にcancelされないようにする LaunchedEffect(key1 = pagerState.currentPage) { scope.launch { if (!pagerState.isScrollInProgress) { pagerState.animateScrollToPage( page = (Int.MAX_VALUE / 2) + pagerState.currentPage % itemCount, ) } } } Column(modifier = modifier) { HorizontalPager( state = pagerState, pageSpacing = 16.dp, pageSize = PageSize.Fixed(200.dp), // 常に左右に隣のコンテンツが見切れているようにする snapPosition = SnapPosition.Center, ) { page -> val index = page % itemCount Box( modifier = Modifier .fillMaxWidth() .height(200.dp) .background(Color.Red), contentAlignment = Alignment.Center, ) { Text("Page: $index") } } Spacer(modifier = Modifier.height(16.dp)) // インジケーター部分の実装 Row( modifier = Modifier .align(Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(6.dp), ) { repeat(itemCount) { index -> val selected = index == pagerState.currentPage % itemCount Box( modifier = Modifier .size(6.dp) .clip(RoundedCornerShape(16.dp)) .background( color = if (selected) { Color(0xFF000000) } else { Color(0xFFB2B2B2) }, shape = RoundedCornerShape(16.dp), ), ) } } } }