Wear OS 向け Compose でのページング

ページングを使用すると、Wear OS デバイスで全画面表示の個別のページ間を水平または垂直にスワイプできます。一般的なユースケースとしては、エクササイズ アプリでワークアウト指標とメディア コントロールの間をスワイプしたり、複数ページのフローをステップ実行したりする場合などがあります。

Compose for Wear OS Material 3 では、HorizontalPagerScaffoldVerticalPagerScaffold がページャー レイアウトを調整し、ページ インジケーターを自動的に配置して、TimeText でトランジションを管理します。各ページは AnimatedPage コンポーザブルでラップされ、丸いディスプレイでページが切り替わる際に、スケーリング、角の丸み、スクリム効果が適用されます。

次のアニメーションは、水平スワイプ中に HorizontalPagerScaffoldAnimatedPage がページをスケーリングしてアニメーション化する方法を示しています。

ページングの Scaffold 階層

Material 3 でページネーションされた画面レイアウトを設計する際は、左右(HorizontalPager)または上下(VerticalPager)のスワイプにかかわらず、外側のコンテナから内側のコンテンツまで、次のコンポーネント階層を使用します。

  1. AppScaffold: アプリのルートにある最も外側のコンテナ(アプリごとに 1 つのみ使用)。グローバル TimeText オーバーレイを固定し、ページ遷移やスワイプして閉じるジェスチャーの際に、時計が画面上部に固定されたままになるようにします。
  2. HorizontalPagerScaffold または VerticalPagerScaffold: ページャレベルで AppScaffold の内部に配置されます。TimeTextHorizontalPageIndicator または VerticalPageIndicator の間の切り替えを調整します。
  3. HorizontalPager または VerticalPager: 共有の PagerState を使用して、スワイプ操作、フリングの物理演算、ロータリー入力のスナップを管理する基盤となるページャー コンテナ。
  4. AnimatedPage: ページャのページ コンテンツ ラムダ内に配置されます。各ページをラップして、PagerState のページのオフセットに基づいて Material 3 のトランジション アニメーション(スケーリングとスクリム効果)を適用します。
  5. ScreenScaffold: 各ページの AnimatedPage内側に配置されます。各ページには、独自の縦方向にスクロール可能なリスト(TransformingLazyColumn など)や独自の EdgeButton を含めることができるため、各ページ内に ScreenScaffold をネストすることで、縦方向のスクロール インジケーター、エッジボタン、contentPadding がその特定のページに属し、そのページとともにスムーズにアニメーション化されるようになります。
AppScaffold (1 per app: anchors global TimeText)
 └── HorizontalPagerScaffold / VerticalPagerScaffold (manages PageIndicator)
      └── HorizontalPager / VerticalPager (manages PagerState & fling behavior)
           └── AnimatedPage (applies scaling & scrim transitions per page)
                └── ScreenScaffold (1 per page: ScrollIndicator & EdgeButton)
                     └── Page Content (Column or TransformingLazyColumn)

横方向ページャを実装する

マテリアル 3 で水平ページャーを実装するには、アプリの AppScaffold 内に HorizontalPagerScaffoldHorizontalPagerAnimatedPageScreenScaffold をネストします。

flingBehaviorPagerScaffoldDefaults.snapWithSpringFlingBehavior を使用すると、マテリアル 3 のスプリング モーションの物理学とレスポンシブ ページのスナップ(HighSnapPositionalThreshold)が適用されます。デフォルトでは、HorizontalPager はロータリー ページのスクロール(rotaryScrollableBehavior = null)を無効にするため、回転するクラウンまたはベゼルの入力でアクティブなページ内の垂直リスト(TransformingLazyColumn など)をスクロールできます。

次のサンプルは、完全な HorizontalPagerScaffold 設定を示しています。

@Composable
fun HorizontalPagerScaffoldSample(navigateBack: () -> Unit) {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 10 })

        HorizontalPagerScaffold(pagerState = pagerState) {
            HorizontalPager(
                state = pagerState,
                flingBehavior =
                    PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                        state = pagerState
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        Column(
                            modifier = Modifier.fillMaxSize(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                        ) {
                            Text(text = "Page #$page")
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(text = "Swipe left and right")
                            if (page == 0) {
                                Spacer(modifier = Modifier.height(16.dp))
                                Button(onClick = navigateBack) { Text("Exit") }
                            }
                        }
                    }
                }
            }
        }
    }
}

水平ページャ内のページに TransformingLazyColumn が含まれている場合は、そのページのスコープ内に別の rememberTransformingLazyColumnState() を作成し、内部の ScreenScaffoldTransformingLazyColumn に渡します。

AppScaffold {
    val pagerState = rememberPagerState(pageCount = { 10 })

    HorizontalPagerScaffold(pagerState = pagerState) {
        HorizontalPager(
            state = pagerState,
            flingBehavior =
                PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                    state = pagerState
                ),
        ) { page ->
            AnimatedPage(pageIndex = page, pagerState = pagerState) {
                val columnState = rememberTransformingLazyColumnState()
                val transformationSpec = rememberTransformationSpec()

                ScreenScaffold(
                    scrollState = columnState,
                ) { contentPadding ->
                    TransformingLazyColumn(
                        state = columnState,
                        contentPadding = contentPadding,
                    ) {
                        item {
                            ListHeader(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .transformedHeight(this, transformationSpec)
                                    .minimumVerticalContentPadding(
                                        ListHeaderDefaults.minimumTopListContentPadding
                                    ),
                                transformation = SurfaceTransformation(transformationSpec),
                            ) {
                                Text(text = "Pager sample")
                            }
                        }
                        item {
                            Card(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .transformedHeight(this, transformationSpec)
                                    .minimumVerticalContentPadding(
                                        CardDefaults.minimumVerticalListContentPadding
                                    ),
                                transformation = SurfaceTransformation(transformationSpec),
                            ) {
                                if (page == 0) {
                                    Text(text = "Page #$page. Swipe right")
                                } else {
                                    Text(text = "Page #$page. Swipe left and right")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

縦方向ページャを実装する

縦型ページャーを使用すると、ユーザーはページ間を上下にスワイプしたり、スマートウォッチの回転式サイドボタンまたはベゼルを使用してページを移動したりできます。

HorizontalPager とは異なり、VerticalPager ではデフォルトでロータリー ページ スナップが有効になっています(RotaryScrollableDefaults.snapBehavior(state))。PagerScaffoldDefaults.snapWithSpringFlingBehaviorflingBehavior に渡し、各ページを AnimatedPageScreenScaffold でラップします。

@Composable
fun VerticalPagerScaffoldSample() {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 10 })

        VerticalPagerScaffold(pagerState = pagerState) {
            VerticalPager(
                state = pagerState,
                flingBehavior =
                    PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                        state = pagerState
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        Column(
                            modifier = Modifier.fillMaxSize(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                        ) {
                            Text(text = "Page #$page")
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(text = "Swipe up and down")
                        }
                    }
                }
            }
        }
    }
}

ページャーの動作をカスタマイズする

アプリのインタラクションのニーズに合わせて、HorizontalPagerScaffoldVerticalPagerScaffold のスナップ感度をカスタマイズできます。

ワークアウトのスナップ感度を調整する

ワークアウトや、ユーザーの粗大運動制御が制限される動きの多いアクティビティでは、誤って画面に触れたり、リューズを少し回転させたりすると、意図せずにページが切り替わることがあります。これらの画面では、ページャの感度を低く設定します。

  • maxFlingPages = 0snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThresholdPagerDefaults.snapFlingBehavior を渡します。そのため、ページをめくるには、画面の大部分を意図的にドラッグする必要があります。
  • ロータリー ページ スナップを有効にする場合は、RotaryScrollableDefaults.snapBehaviorsnapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity を設定して、次のページにスナップする前に回転を増やすようにします。
@Composable
fun HorizontalPagerScaffoldWithLowSensitivitySample(navigateBack: () -> Unit) {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 3 })

        HorizontalPagerScaffold(pagerState = pagerState) {
            HorizontalPager(
                state = pagerState,
                flingBehavior =
                    PagerDefaults.snapFlingBehavior(
                        state = pagerState,
                        maxFlingPages = 0,
                        snapPositionalThreshold =
                            PagerScaffoldDefaults.LowSnapPositionalThreshold,
                    ),
                rotaryScrollableBehavior =
                    RotaryScrollableDefaults.snapBehavior(
                        pagerState = pagerState,
                        snapSensitivity =
                            RotaryScrollableDefaults.LowSnapSensitivity,
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        // Page content
                    }
                }
            }
        }
    }
}

完全なサンプル実装については、android/snippets リポジトリの Pager.kt と AndroidX リポジトリの PagerScaffoldSample.kt をご覧ください。