リストを使用すると、Wear OS デバイス上でユーザーが選択肢の中からアイテムを選択できるようになります。
多くの Wear OS デバイスは円形の画面であるため、画面の上部や下部付近に表示されるリストアイテムが見づらくなっています。このため、Wear OS 向け Compose には、スケーリングとモーフィングのアニメーションをサポートする TransformingLazyColumn という LazyColumn クラスのバージョンが含まれています。アイテムが端に移動すると、アイテムは小さくなり、フェードアウトします。
推奨される拡大縮小とスクロールの効果を適用するには:
- Compose でアイテムが画面をスクロールする際の高さの変化を計算できるようにするには、
Modifier.transformedHeightを使用します。 transformation = SurfaceTransformation(transformationSpec)を使用して、アイテムのコンテンツの縮小などの視覚効果を適用します。transformationをパラメータとして受け取らないコンポーネント(Textなど)には、カスタムTransformationSpecを使用します。
次のアニメーションは、リスト要素が画面の上部と下部に近づくにつれて、どのように拡大縮小され、形状が変化するかを示しています。
次のコード スニペットは、TransformingLazyColumn レイアウトを使用して、さまざまな Wear OS 画面サイズで美しく表示されるコンテンツを作成する方法を示しています。
このスニペットでは、minimumVerticalContentPadding 修飾子の使用方法も示しています。リストの上下に適切なパディングを適用するには、リストアイテムにこの修飾子を設定する必要があります。
スクロール インジケーターを表示するには、ScreenScaffold と TransformingLazyColumn の間で columnState を共有します。
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 = "Header") } } // ... other items item { Button( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), transformation = SurfaceTransformation(transformationSpec), onClick = { /* ... */ }, icon = { Icon( imageVector = Icons.Default.Build, contentDescription = "build", ) }, ) { Text( text = "Build", maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } }
スナップ&フリング エフェクトを追加する
スナップとは、ユーザーがスクロールまたはフリング ジェスチャーを完了したときに、リストが特定のポイント(通常は画面の中央)に正確に配置された状態で停止することを保証するものです。丸い画面では、アイテムが中央から離れるにつれて拡大縮小や変形が行われるため、スナップは、最も関連性の高いアイテムが最適な表示領域で完全に表示され、読み取れるようにするうえで特に便利です。スナップとフリングの動作を追加するには、flingBehavior パラメータを TransformingLazyColumnDefaults.snapFlingBehavior(columnState) に設定します。物理的なクラウンまたはベゼルを使用する際に一貫した操作性を実現するため、RotaryScrollableDefaults.snapBehavior(columnState) を使用して rotaryScrollableBehavior を一致するように設定します。
val columnState = rememberTransformingLazyColumnState() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) ) { // ... // ... } }
レイアウトを反転する
デフォルトでは、スクロール可能なリストは上端に固定されます。ユーザーが標準リストの最下部までスクロールし、新しいアイテムが末尾に追加された場合、リストはユーザーの現在のアイテムのビューを維持します。たとえば、ユーザーが画面の下部にあるアイテム 10 を表示しているときにアイテム 11 が追加されると、ビューはアイテム 10 にフォーカスされたままになり、アイテム 11 は現在のビューの下の画面外に表示されます。
メッセージ アプリケーションやライブログなどのユースケースでは、通常、この動作は望ましくありません。新しいアイテムが届いた場合、ユーザーは通常、リストの末尾にいる場合は最新のコンテンツをすぐに表示したいと考えます。一度に多くのアイテムが届いた場合、リストはスキップして一番下の最新のアイテムを表示します(つまり、ユーザーがスクロールして戻らない限り、中間にあるアイテムはまったく表示されない可能性があります)。
これらのユースケースをサポートするため、TransformingLazyColumn では reverseLayout = true を設定してレイアウトを反転させることができます。これにより、リストのアンカーが上端から下端に変更されます。
便宜上、reverseLayout = true を設定すると、アイテムの視覚的な順序とスクロール ジェスチャーの方向も反転します。
- アイテムは下から上に構成されます。つまり、インデックス 0 は画面の下部に表示されます。
- 上にスクロールすると、インデックスの大きいアイテムが表示されます。
レイアウトの反転とともにスナップとフリングの動作を追加するには、次のスニペットに示すように、flingBehavior と rotaryScrollableBehavior を組み合わせます。
val columnState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, contentPadding = contentPadding, reverseLayout = true, modifier = Modifier.fillMaxWidth() ) { items(10) { index -> Button( label = { Text( text = "Item ${index + 1}" ) }, onClick = {}, modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) } item { // With reverseLayout = true, the last item declared appears at the top. ListHeader( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) { Text("Header") } } } }
次の画像は、通常のリストと反転リストの違いを示しています。

リストの端にあるボタン
マテリアル 3 では、リストの下部にエッジに沿ったボタンである EdgeButton を追加できます。ただし、これを TransformingLazyColumn 内のアイテムとして追加するのではなく、ScreenScaffold の edgeButton スロットを使用するようにしてください。
edgeButton スロットを使用すると、ボタンが画面の下部に正しく配置され、リストがスクロールされたときに適切に動作します。
次のコード スニペットは、EdgeButton を実装する方法を示しています。
val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, edgeButton = { EdgeButton( onClick = { /* TODO */ }, modifier = Modifier.scrollable( columnState, orientation = Orientation.Vertical, reverseDirection = true, // Apply overscroll to the EdgeButton for proper scrolling behavior. overscrollEffect = rememberOverscrollEffect(), ) ) { Text("More") } } ) { contentPadding -> TransformingLazyColumn( contentPadding = contentPadding, state = columnState, ) { // ... // ... } }
リストの SwipeToReveal
SwipeToReveal コンポーネントを使用すると、スワイプしてリストアイテムのアクション(Card や Chip など)にアクセスできます。通常、スワイプすると、横から 1 つまたは 2 つのアクション ボタン([削除] や [その他] など)が表示されます。
TransformingLazyColumn 内で SwipeToReveal を使用する場合は、次のガイドラインに沿ってください。
- スクロール時にリセット: ユーザーがリストをスクロールしたときに、スワイプして開いたアイテムを覆われた状態にリセットします。
- 高さの一貫性: 操作ボタンの高さを、内側のスワイプ アイテム(
ButtonまたはCard)に合わせて設定し、一貫した外観を確保します。 - コンテナを変換する:
transformedHeight修飾子とtransformationSpecをSwipeToRevealコンポーネント自体に適用します。 - 二重変換しない: 内側のスワイプ アイテム(
SwipeToRevealコンテナ内のカードまたはボタン)にtransformedHeight修飾子またはtransformation修飾子を適用しないでください。
リスト内のカスタム コンポーザブル
TransformingLazyColumn 用のカスタム サーフェス コンポーネントを構築する際は、次のベスト プラクティスに沿って、コンポーザブルがディスプレイの端付近でスムーズにスケーリング、フェード、モーフィングされるようにします。
SurfaceTransformationを公開: 標準の Wear Compose マテリアル 3 コンポーネント(CardやButtonなど)に合わせて、オプションのSurfaceTransformationパラメータ(デフォルトはnull)を受け入れます。これにより、TransformingLazyColumnの呼び出し元はSurfaceTransformation(transformationSpec)を渡すことができ、コンポーネントはリスト外で正常に動作します。- 呼び出しコードで
Modifier.transformedHeightを最初に適用する: カスタム コンポーザブルをTransformingLazyColumnに配置する場合は、呼び出しコードの修飾子チェーンの最初の修飾子としてModifier.transformedHeight(this, transformationSpec)を渡します。SurfaceTransformationは視覚的なスケーリングとフェード効果を適用しますが、transformedHeightは、リスト レイアウトにアイテムの高さが縮小されたときに再計算するように指示するうえで重要です。 - 変換レイヤ、呼び出し元
modifier、ペインタを順番に適用:- コンテナ変換レイヤ:
Modifier.graphicsLayerとapplyContainerTransformation()でルート コンテナの修飾子チェーンを開始し、背景とコンテンツの両方がスケーリングされ傾斜した座標空間内に描画されるようにします。 - 呼び出し元
modifier: 内部のサイズ設定やパディングの前に、呼び出し元から渡されたmodifierパラメータ(Modifier.transformedHeightを含む)を適用します。 - 変換がない場合のシェイプクリップ:
transformationがnullの場合、背景を描画する前にModifier.clip(shape)を適用します。createContainerPainter()によって返されるペインタは、シェイプにクリップされますが、プレーンなペインタはクリップされません。そのため、これがないと、リストの外側の背景が角の丸くない四角で描画されます。 - 背景の変形ペインタ:
Modifier.drawBehindとcreateContainerPainter()から作成されたペインタを使用して、コンテナレイヤの内部に背景を描画します。 - コンテンツ変換レイヤ:
applyContentTransformation()とクリップを使用して、コンテナの形状に 2 つ目のModifier.graphicsLayerを適用し、内側のコンテンツがベゼルに近づくにつれて早くフェードアウトするようにします。
- コンテナ変換レイヤ:
次のスニペットは、これらの変換を順番に適用するカスタム BoardingPassCard コンポーザブルを実装する方法を示しています。
@Composable fun BoardingPassCard( flightNumber: String, origin: String, destination: String, gate: String, seat: String, departureTime: String, modifier: Modifier = Modifier, transformation: SurfaceTransformation? = null, shape: Shape = RoundedCornerShape(18.dp), statusBadge: @Composable () -> Unit = {} ) { // 1. Create morphing container painter val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer) val finalPainter = if (transformation != null) { remember(transformation, backgroundPainter, shape) { transformation.createContainerPainter(backgroundPainter, shape, border = null) } } else { backgroundPainter } Column( modifier = Modifier // 2a. Container layer: Scales, fades, and tilts the whole card surface .then( if (transformation != null) { Modifier.graphicsLayer { transformation.run { applyContainerTransformation() } } } else Modifier ) // 2b. Caller modifier: Includes Modifier.transformedHeight in a list .then(modifier) .fillMaxWidth() // 2c. Shape clip: Only needed without a transformation, because the // painter from createContainerPainter clips itself to the shape .then(if (transformation == null) Modifier.clip(shape) else Modifier) // 2d. Morphing background: Drawn inside the transformed container layer .drawBehind { with(finalPainter) { draw(size) } } // 2e. Content layer: Fades content earlier and clips children to shape .then( if (transformation != null) { Modifier.graphicsLayer { this.shape = shape this.clip = true transformation.run { applyContentTransformation() } } } else Modifier ) .padding(horizontal = 14.dp, vertical = 10.dp) ) { // Card content goes here } }
SurfaceTransformation(transformationSpec) とともに Modifier.transformedHeight を最初の修飾子として渡すことで、TransformingLazyColumn 内で BoardingPassCard を使用できます。
@Composable fun BoardingPassListSample(flights: List<FlightInfo>) { val listState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold(scrollState = listState) { contentPadding -> TransformingLazyColumn( state = listState, contentPadding = contentPadding, modifier = Modifier.fillMaxSize() ) { items(flights.size) { index -> val flight = flights[index] BoardingPassCard( flightNumber = flight.number, origin = flight.origin, destination = flight.destination, gate = flight.gate, seat = flight.seat, departureTime = flight.time, modifier = Modifier .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding( CardDefaults.minimumVerticalListContentPadding ), transformation = SurfaceTransformation(transformationSpec) ) } } } }
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Wear OS 向け Compose の Codelab
- リストとグリッド
- Compose でビューを使用する