Compose の HorizontalPager と Snap

HorizontalPager には Snap というものがある。

Snap によって、ページのスクロールをどこで止めるかを変えられる。

PageSize が Fill だとセルをめいいっぱい左右使ってしまって挙動がわからないので Fixed にしてしまう。

HorizontalPager(
    pageSize = PageSize.Fixed(100.dp), // これ

そこにセルを追加してあげる。PageSize.Fixed(100.dp) と同じサイズを size に指定している。

HorizontalPager(
    pageSize = PageSize.Fixed(100.dp),
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.Red),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            text = "$it",
            modifier = Modifier.padding(16.dp)
        )
    }
}

この状態でそれぞれの挙動を見ると差がわかる。

SnapPosition.Start

デフォルトのやつ。Start 側で止まる。

HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp),
    pageSpacing = 12.dp,
    snapPosition = SnapPosition.Start, // なくてもいい。デフォルトで Start になる。
    modifier = modifier.fillMaxWidth()
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.Red),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            text = "$it",
        )
    }
}

以下は6番目のセルが Start 側で止まっている様子。

SnapPosition.Center

Center で止まる。

HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp),
    pageSpacing = 12.dp,
    snapPosition = SnapPosition.Center,
    modifier = modifier.fillMaxWidth()
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.Red),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            text = "$it",
        )
    }
}

以下は6番目のセルが Center 側で止まっている様子。

SnapPosition.End

End 側で止まる。

HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp),
    pageSpacing = 12.dp,
    snapPosition = SnapPosition.End,
    modifier = modifier.fillMaxWidth()
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.Red),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            text = "$it",
        )
    }
}

以下は6番目のセルが End 側で止まっている様子。

animateScrollToPage との兼ね合い

HorizontalPager の状態を管理する PagerState には animateScrollToPage というメソッドがあって、ページ番号を指定すればアニメーション付きでその場所までスクロールしてくれる。

PagerState  |  API reference  |  Android Developers

ボタンを作ってみる。animateScrollToPage を呼び出すボタンだ。

val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage = 3) { 100 }

Column {
    MyPager(
        pagerState = pagerState,
        modifier = Modifier.padding(innerPadding),
    )
    Button(onClick = {
        coroutineScope.launch {
            pagerState.animateScrollToPage(6) // これ
        }
    }) {
        Text("animateScrollToPage(6)")
    }
}

SnapPosition.Start の状態で実行してみる。アニメーションして、6番目のセルが Start 位置までスクロールされる。

ところでこの animateScrollToPage だけど、Snap.Center と組み合わせると不可解なことが起こる。

一度目の押下ではちゃんと 6 が center になるけど、2度目以降の押下では 6 が start になる。

これは現状の stable な Compose Foundation バージョン 1.7.8 で snap 計算がバグっているからみたいで、1.8.0-alpha01 から解消されている。

https://developer.android.com/jetpack/androidx/releases/compose-foundation?hl=ja#1.8.0-alpha01

commit を見たらいくつかの snap 関連の修正が確認できる。