ピンチ操作によるズーム ジェスチャーを実装して、アプリでスケーラブルなコンテンツをサポートします。これは、ユーザー補助機能を改善するための標準的でプラットフォームに一貫した方法であり、ユーザーはニーズに合わせてテキストや UI 要素のサイズを直感的に調整できます。アプリでは、きめ細かい制御とコンテキストに応じた動作により、カスタムのスケーリング動作を定義できます。これにより、画面拡大などのシステムレベルの機能よりも、ユーザーがより迅速に発見できるエクスペリエンスを提供できます。
スケーリング戦略を選択する
このガイドで説明する戦略では、UI が画面の幅に合わせてリフローされ、再編成されます。これにより、水平方向のパンや、長いテキスト行を読むために必要な「ジグザグ」の動きが不要になり、ユーザー補助機能が大幅に向上します。
参考情報: 調査によると、ロービジョンのユーザーにとって、リフローされたコンテンツは、2 次元のパンが必要なインターフェースよりもはるかに読みやすく、操作しやすいことが確認されています。詳しくは、モバイル デバイスでのパン&スキャンとリフロー可能なコンテンツの比較をご覧ください。
すべての要素またはテキスト要素のみをスケーリングする
次の表に、各スケーリング戦略の視覚効果を示します。
| 戦略 | 密度スケーリング | フォントの拡大と縮小 |
|---|---|---|
動作 |
すべてを比例的にスケーリングします。コンテンツはコンテナに合わせてリフローされるため、ユーザーはすべてのコンテンツを表示するために水平方向にパンする必要はありません。 |
テキスト要素にのみ影響します。全体のレイアウトとテキスト以外のコンポーネントのサイズは変わりません。 |
スケーリングの対象 |
すべての視覚要素: テキスト、コンポーネント(ボタン、アイコン)、画像、レイアウトの間隔(パディング、マージン) |
テキストのみ |
デモ |
推奨事項
視覚的な違いを確認したところで、次の表を参考にしてトレードオフを比較検討し、コンテンツに最適な戦略を選択してください。
UI タイプ |
推奨される戦略 |
理由: |
読書に特化したレイアウト 例: ニュース記事、メッセージ アプリ |
密度またはフォント スケーリング |
インライン画像を含むコンテンツ領域全体をスケーリングするには、密度スケーリングが推奨されます。 テキストのみを拡大縮小する必要がある場合は、フォント スケーリングが簡単な代替手段となります。 |
視覚的に構造化されたレイアウト 例: アプリストア、ソーシャル メディア フィード |
密度スケーリング |
カルーセルやグリッド内の画像とテキストの視覚的な関係を維持します。リフローの性質により、ネストされたスクロール要素と競合する水平方向のパンを回避できます。 |
Jetpack Compose でスケーリング ジェスチャーを検出する
ユーザーが拡大縮小できるコンテンツをサポートするには、まずマルチタッチ ジェスチャーを検出する必要があります。Jetpack Compose では、Modifier.transformable を使用してこれを行うことができます。
transformable 修飾子は、最後のジェスチャー イベント以降の zoomChange 差分を提供する高レベル API です。これにより、状態更新ロジックが直接累積(scale *= zoomChange など)に簡素化され、このガイドで説明する適応型スケーリング戦略に最適です。
実装の例
次の例は、密度スケーリングとフォント スケーリングの戦略を実装する方法を示しています。
密度スケーリング
このアプローチでは、UI 領域のベース density をスケーリングします。その結果、パディング、間隔、コンポーネント サイズなど、レイアウトに基づくすべての測定値が、画面サイズや解像度が変更されたかのようにスケーリングされます。テキストサイズも密度に依存するため、比例してスケーリングされます。この戦略は、特定の領域内のすべての要素を均一に拡大し、UI の全体的な視覚的なリズムと比率を維持する場合に効果的です。
private class DensityScalingState( // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x. private val minScale: Float = 0.75f, private val maxScale: Float = 3.5f, private val currentDensity: Density ) { val transformableState = TransformableState { zoomChange, _, _ -> scaleFactor.floatValue = (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale) } val scaleFactor = mutableFloatStateOf(1f) fun scaledDensity(): Density { return Density( currentDensity.density * scaleFactor.floatValue, currentDensity.fontScale ) } } @Composable fun DensityScalingSample() { val currentDensity = LocalDensity.current val scaleState = remember(currentDensity) { DensityScalingState(currentDensity = currentDensity) } Box( modifier = Modifier .fillMaxSize() .transformable(state = scaleState.transformableState), contentAlignment = Alignment.TopCenter ) { CompositionLocalProvider( LocalDensity provides scaleState.scaledDensity() ) { DemoCard() } } }
フォントの拡大と縮小
この戦略はよりターゲットを絞ったもので、fontScale 係数のみを変更します。その結果、テキスト要素のみが拡大または縮小され、コンテナ、パディング、アイコンなど、他のすべてのレイアウト コンポーネントは固定サイズのままになります。この戦略は、読書に特化したアプリでテキストの読みやすさを向上させるのに適しています。
class FontScaleState( // Note: For accessibility, typical min/max values are ~0.75x and ~3.5x. private val minScale: Float = 0.75f, private val maxScale: Float = 3.5f, private val currentDensity: Density ) { val transformableState = TransformableState { zoomChange, _, _ -> scaleFactor.floatValue = (scaleFactor.floatValue * zoomChange).coerceIn(minScale, maxScale) } val scaleFactor = mutableFloatStateOf(1f) fun scaledFont(): Density { return Density( currentDensity.density, currentDensity.fontScale * scaleFactor.floatValue ) } } @Composable fun FontScalingSample() { val currentDensity = LocalDensity.current val scaleState = remember { FontScaleState(currentDensity = currentDensity) } Box( modifier = Modifier .fillMaxSize() .transformable(state = scaleState.transformableState), contentAlignment = Alignment.TopCenter ) { CompositionLocalProvider( LocalDensity provides scaleState.scaledFont() ) { DemoCard() } } }
共有デモ UI
これは、前述の 2 つの例で使用されている共有 DemoCard コンポーザブルで、さまざまなスケーリング動作をハイライト表示します。
@Composable private fun DemoCard() { Card( modifier = Modifier .width(360.dp) .padding(16.dp), shape = RoundedCornerShape(12.dp) ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text("Demo Card", style = MaterialTheme.typography.headlineMedium) var isChecked by remember { mutableStateOf(true) } Row(verticalAlignment = Alignment.CenterVertically) { Text("Demo Switch", Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge) Switch(checked = isChecked, onCheckedChange = { isChecked = it }) } Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.Person, "Icon", Modifier.size(32.dp)) Spacer(Modifier.width(8.dp)) Text("Demo Icon", style = MaterialTheme.typography.bodyLarge) } Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Box( Modifier .width(100.dp) .weight(1f) .height(80.dp) .background(Color.Blue) ) Box( Modifier .width(100.dp) .weight(1f) .height(80.dp) .background(Color.Red) ) } Text( "Demo Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit," + " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Justify ) } } }
ヒントと考慮事項
より洗練された、アクセスしやすいエクスペリエンスを作成するには、次の推奨事項を検討してください。
- ジェスチャー以外のスケール コントロールの提供を検討する: ジェスチャーが困難なユーザーもいます。このようなユーザーをサポートするには、ジェスチャーに依存しない、スケールを調整またはリセットする別の方法を提供することを検討してください。
- すべてのスケールに対応する: アプリ内のスケーリングとシステム全体のフォントまたはディスプレイの設定の両方に対して UI をテストします。アプリのレイアウトが、コンテンツの破損、重複、隠しコンテンツを発生させることなく、正しく適応することを確認します。アダプティブ レイアウトの作成方法の詳細を確認する。