Compose でのページャー

コンテンツを水平方向または垂直方向にフリップするには、 HorizontalPager コンポーザブルと VerticalPager コンポーザブルを使用します。これらは、ビュー システムの ViewPager と同様の機能を備えています。デフォルトでは、HorizontalPager は画面の幅全体を使用し、VerticalPager は画面の高さ全体を使用します。また、ページャーは一度に 1 ページしかフリングしません。これらのデフォルトはすべて構成可能です。

HorizontalPager

左右に水平スクロールするページャーを作成するには、HorizontalPager を使用します。

図 1. HorizontalPager
のデモ

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

VerticalPager

上下にスクロールするページャーを作成するには、VerticalPager を使用します。

図 2. VerticalPager
のデモ

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

遅延作成

HorizontalPagerVerticalPager の両方のページは、必要に応じて遅延コンポーズされ 、レイアウトされます。ユーザーがページをスクロールすると、コンポーザブルは不要になったページを削除します。

画面外のページをさらに読み込む

デフォルトでは、ページャーは画面に表示されているページのみを読み込みます。画面外のページをさらに読み込むには、beyondBoundsPageCount を 0 より大きい値に設定します。

ページャー内の項目にスクロールする

ページャー内の特定のページにスクロールするには、PagerState オブジェクト を rememberPagerState() を使用して作成し、ページャーに state パラメータとして渡します。` CoroutineScope` 内で、この状態に対して `PagerState#scrollToPage()` を呼び出すことができます。

val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

ページにアニメーションを表示する場合は、 PagerState#animateScrollToPage() 関数を使用します。

val pagerState = rememberPagerState(pageCount = {
    10
})

HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.animateScrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

ページの状態の変化に関する通知を受け取る

PagerState には、ページに関する情報を含む 3 つのプロパティ( currentPagesettledPagetargetPage)があります。

  • currentPage: スナップ位置に最も近いページ。デフォルトでは、スナップ位置はレイアウトの先頭にあります。
  • settledPage: アニメーションやスクロールが実行されていないときのページ番号。ページがスナップ位置に十分に近い場合、currentPage はすぐに更新されますが、settledPage はすべてのアニメーションが完了するまで同じままです。この点が currentPage プロパティとは異なります。
  • targetPage: スクロール移動の停止位置の候補。

snapshotFlow 関数を使用すると、これらの変数の変更を監視して対応できます。たとえば、ページが切り替わるたびに分析イベントを送信するには、次のようにします。

val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

ページ インジケーターを追加する

ページにインジケーターを追加するには、PagerState オブジェクトを使用して、複数のページの中から選択されているページに関する情報を取得し、カスタム インジケーターを描画します。

たとえば、円形のインジケーターを作成するには、pagerState.currentPage を使用して円の数を繰り返し、ページが選択されているかどうかに基づいて円の色を変更します。

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    modifier = Modifier.fillMaxSize()
) { page ->
    // Our page content
    Text(
        text = "Page: $page",
    )
}
Row(
    Modifier
        .wrapContentHeight()
        .fillMaxWidth()
        .align(Alignment.BottomCenter)
        .padding(bottom = 8.dp),
    horizontalArrangement = Arrangement.Center
) {
    repeat(pagerState.pageCount) { iteration ->
        val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
        Box(
            modifier = Modifier
                .padding(2.dp)
                .clip(CircleShape)
                .background(color)
                .size(16.dp)
        )
    }
}

コンテンツの下に丸いインジケーターが表示されているページャー
図 3. コンテンツの下に円形のインジケーターを表示するページャー

コンテンツに項目スクロール効果を適用する

一般的なユースケースは、スクロール位置を使用してページャー項目に効果を適用することです。ページが選択したページからどのくらい離れているかを確認するには、 PagerState.currentPageOffsetFractionを使用します。選択したページからの距離に基づいて、コンテンツに変換効果を適用できます。

図 4. ページャー コンテンツに変換を適用する

たとえば、中央からの距離に基づいて項目の不透明度を調整するには、ページャー内の項目で Modifier.graphicsLayer を使用して alpha を変更します。

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }
    ) {
        // Card content
    }
}

カスタム ページサイズ

デフォルトでは、HorizontalPagerVerticalPager はそれぞれ幅全体または高さ全体を使用します。pageSize 変数を FixedFill(デフォルト)、またはカスタム サイズ計算に設定できます。

たとえば、固定幅のページを 100.dp に設定するには、次のようにします。

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

ビューポートのサイズに基づいてページをサイズ設定するには、カスタム ページサイズ計算を使用します。カスタム PageSize オブジェクトを作成し、項目間の間隔を考慮して availableSpace を 3 で割ります。

private val threePagesPerViewport = object : PageSize {
    override fun Density.calculateMainAxisPageSize(
        availableSpace: Int,
        pageSpacing: Int
    ): Int {
        return (availableSpace - 2 * pageSpacing) / 3
    }
}

コンテンツのパディング

HorizontalPagerVerticalPager の両方でコンテンツのパディングを変更できます。これにより、ページの最大サイズと配置に影響します。

たとえば、start パディングを設定すると、ページが末尾に揃えられます。

開始パディングのあるページャ。コンテンツは末尾に揃えられている
図 5. 開始パディングのあるページャー。

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp),
) { page ->
    // page content
}

start パディングと end パディングの両方を同じ値に設定すると、項目が水平方向に中央揃えになります。

コンテンツが中央に配置された、開始パディングと終了パディングのあるページャ
図 6. 水平パディングのあるページャー。

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
    // page content
}

end パディングを設定すると、ページが先頭に揃えられます。

開始パディングと終了パディングがあり、コンテンツが開始位置に揃えられているページャ
図 7. 終了パディングのあるページャー。

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp),
) { page ->
    // page content
}

top 値と bottom 値を設定すると、VerticalPager でも同様の効果が得られます。値 32.dp は例としてのみ使用されます。パディングの各ディメンションには任意の値を設定できます。

スクロール動作をカスタマイズする

デフォルトの HorizontalPager コンポーザブルと VerticalPager コンポーザブルは、スクロール操作がページャーでどのように機能するかを指定します。ただし、pagerSnapDistanceflingBehavior などのデフォルトをカスタマイズして変更できます。

スナップ距離

デフォルトでは、HorizontalPagerVerticalPager は、フリング操作でスクロールできるページの最大数を一度に 1 ページに設定します。これを変更するには、pagerSnapDistanceflingBehavior で設定します。

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        state = pagerState,
        pageSize = PageSize.Fixed(200.dp),
        beyondViewportPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}

自動進行ページャーを作成する

このセクションでは、Compose でページ インジケーターを使用して自動進行ページャーを作成する方法について説明します。項目のコレクションは自動的に水平方向にスクロールしますが、ユーザーは項目間を手動でスワイプすることもできます。ユーザーがページャーを操作すると、自動進行が停止します。

基本的な例

次のスニペットを組み合わせると、各ページが異なる色でレンダリングされるビジュアル インジケーターを備えた基本的な自動進行ページャーの実装が作成されます。

@Composable
fun AutoAdvancePager(pageItems: List<Color>, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        val pagerState = rememberPagerState(pageCount = { pageItems.size })
        val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()

        val pageInteractionSource = remember { MutableInteractionSource() }
        val pageIsPressed by pageInteractionSource.collectIsPressedAsState()

        // Stop auto-advancing when pager is dragged or one of the pages is pressed
        val autoAdvance = !pagerIsDragged && !pageIsPressed

        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000)
                    val nextPage = (pagerState.currentPage + 1) % pageItems.size
                    pagerState.animateScrollToPage(nextPage)
                }
            }
        }

        HorizontalPager(
            state = pagerState
        ) { page ->
            Text(
                text = "Page: $page",
                textAlign = TextAlign.Center,
                modifier = modifier
                    .fillMaxSize()
                    .background(pageItems[page])
                    .clickable(
                        interactionSource = pageInteractionSource,
                        indication = LocalIndication.current
                    ) {
                        // Handle page click
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }

        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

コードに関する主なポイント

  • AutoAdvancePager 関数は、自動進行する水平ページング ビューを作成します。入力として Color オブジェクトのリストを受け取ります。これは、各ページの背景色として使用されます。
  • pagerStaterememberPagerState を使用して作成されます。これは、ページャーの状態 を保持します。
  • pagerIsDraggedpageIsPressed はユーザー インタラクションを追跡します。
  • LaunchedEffect は、 ユーザーがページャーをドラッグするか、いずれかのページを押さない限り、2 秒ごとにページャーを自動進行します。
  • HorizontalPager はページのリストを表示します。各ページには、ページ番号を表示する Text コンポーザブルが含まれています。この修飾子はページを塗りつぶし、pageItems から背景色を設定し、ページをクリック可能にします。

@Composable
fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        Row(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            repeat(pageCount) { iteration ->
                val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray
                Box(
                    modifier = modifier
                        .padding(2.dp)
                        .clip(CircleShape)
                        .background(color)
                        .size(16.dp)
                )
            }
        }
    }
}

コードに関する主なポイント

  • A Box コンポーザブルはルート要素として機能し、ページ インジケーターを水平方向に配置する Row を 含みます。
  • カスタム ページ インジケーターは円の行として表示されます。CircleShape にクリップされた各 Box はページを表します。
  • 現在のページの円は DarkGray で色付けされ、他の円は LightGray です。currentPageIndex パラメータは、濃いグレーでレンダリングする円を指定します。

結果

この動画では、前のスニペットの基本的な自動進行ページャーが表示されます。

図 8. 各ページの進行に 2 秒の遅延がある自動進行ページャー。

参考情報