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

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

デフォルトでは、Pager は画面に表示されているページのみを読み込みます。オフスクリーンでより多くのページを読み込むには、beyondBoundsPageCount を 0 より大きい値に設定します。

ページャー内のアイテムまでスクロールする

ページャーの特定のページにスクロールするには、rememberPagerState() を使用して PagerState オブジェクトを作成し、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 プロパティとは異なります。currentPage は、ページがスナップ位置に十分に近づくとすぐに更新されますが、settledPage はすべてのアニメーションの実行が完了するまで同じままです。
  • 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. Pager コンテンツに変換を適用する

たとえば、アイテムの不透明度を中央からの距離に基づいて調整するには、ページャ内のアイテムの 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 パディングを設定すると、ページが末尾に揃えられます。

開始パディングのあるページャで、コンテンツが末尾に揃えられている様子

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

startend の両方のパディングを同じ値に設定すると、アイテムが水平方向に中央揃えになります。

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

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

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

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

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

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

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

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

スナップ距離

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

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 オブジェクトのリストを入力として受け取ります。
  • pagerState は、ページャーの状態を保持する rememberPagerState を使用して作成されます。
  • 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)
                )
            }
        }
    }
}

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

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

結果

この動画は、前のスニペットの基本的な自動進行ページャを表示しています。

図 1: 各ページ遷移の間に 2 秒の遅延がある自動進行ページャ。

参考情報