android-design-guidelines について
Material Design 3に基づく、Android UIの設計・実装向けスキルです。ダイナミックカラー、ナビゲーション、レスポンシブレイアウト、文字、折りたたみ端末への対応を扱います。
主な機能
- 01ダイナミックカラーとテーマロールのルール
- 02画面サイズに応じたナビゲーション選択
- 03ウィンドウサイズクラスによるレイアウト指針
- 04エッジツーエッジとシステムバーのインセット指針
- 05折りたたみレイアウトの推奨事項
活用シーン
- 01Androidアプリのダイナミックテーマ設定
- 02スマートフォン、タブレット、大画面のナビゲーション設計
- 03ウィンドウサイズに応じたレイアウト設計
- 04折りたたみ端末のヒンジ周辺の配置確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ehmo/ platform-design-skills --skill android-design-guidelinesインストール後、Claude Code で/android-design-guidelinesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdmetadata.jsonrules/_sections.md詳細
Android プラットフォームのデザインガイドライン — Material Design 3
1. Material You とテーマ設定 [CRITICAL]
1.1 ダイナミックカラー
ユーザーの壁紙から生成されるダイナミックカラーを有効にします。ダイナミックカラーは Android 12 以降のデフォルトであり、主要なテーマ設定方法として使用してください。
// Compose: Dynamic color theme
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}
<style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">
<item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.DayNight</item>
</style>
ルール:
- R1.1:Android 12 未満のデバイス向けに、必ず静的なカラーの代替スキームを用意してください。
- R1.2:コンポーネントにカラーの16進数値をハードコードしないでください。必ずテーマのカラー役割を参照してください。
- R1.3:ダイナミックカラーの調和を確認するため、少なくとも3種類の壁紙でテストしてください。
1.2 カラーの役割
Material 3 は、体系化されたカラーの役割を定義しています。見た目ではなく、意味に基づいて使用してください。
| 役割 | 用途 | 対応する役割 |
|---|---|---|
primary | 主要なアクション、アクティブ状態、FAB | onPrimary |
primaryContainer | 目立ちすぎない主要要素 | onPrimaryContainer |
secondary | 補助的な UI、フィルターチップ | onSecondary |
secondaryContainer | ナビゲーションバーのアクティブインジケーター | onSecondaryContainer |
tertiary | アクセント、コントラスト、補色 | onTertiary |
tertiaryContainer | 入力フィールド、目立ちすぎないアクセント | onTertiaryContainer |
surface | 背景、カード、シート | onSurface |
surfaceVariant | 装飾要素、区切り線 | onSurfaceVariant |
error | エラー状態、破壊的なアクション | onError |
errorContainer | エラーの背景 | onErrorContainer |
outline | 境界線、区切り線 | — |
outlineVariant | 目立ちにくい境界線 | — |
inverseSurface | スナックバーの背景 | inverseOnSurface |
// Correct: semantic color roles
Text(
text = "Error message",
color = MaterialTheme.colorScheme.error
)
Surface(color = MaterialTheme.colorScheme.errorContainer) {
Text(text = "Error detail", color = MaterialTheme.colorScheme.onErrorContainer)
}
// WRONG: hardcoded colors
Text(text = "Error", color = Color(0xFFB00020)) // Anti-pattern
ルール:
- R1.4:すべての前景要素には、その背景に対応する
onカラーの役割を使用してください(例:primaryの背景にはonPrimaryのテキストを使用します)。 - R1.5:背景には
surfaceとそのバリエーションを使用してください。広い背景領域にprimaryやsecondaryを使用しないでください。 - R1.6:
tertiaryはアクセントや補色によるコントラストに限り、控えめに使用してください。
1.3 ライトテーマとダークテーマ
ライトテーマとダークテーマの両方をサポートします。デフォルトではシステム設定に従ってください。
// Compose: Detect system theme
val darkTheme = isSystemInDarkTheme()
ルール:
- R1.7:ライトテーマとダークテーマの両方を必ずサポートしてください。ライトテーマのみでリリースしないでください。
- R1.8:ダークテーマのサーフェスには、純粋な黒(#000000)ではなく、標高に基づく色調マッピングを使用してください。自動的に処理される
surfaceのカラー役割を使用してください。 - R1.9:アプリの設定で、手動のテーマ切り替え(システム/ライト/ダーク)を提供してください。
1.4 カスタムカラーのシード
ブランディングでカスタムカラーが必要な場合は、シードカラーを指定し、Material Theme Builder を使用して色調パレットを生成してください。
// Custom color scheme with brand seed
private val BrandLightColorScheme = lightColorScheme(
primary = Color(0xFF1B6D2F),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFA4F6A8),
onPrimaryContainer = Color(0xFF002107),
// ... generate full palette from seed
)
ルール:
- R1.10:Material Theme Builder を使用して、シードカラーから色調パレットを生成してください。個々の色調を手作業で選ばないでください。
- R1.11:カスタムカラーを使用する場合も、デフォルトではダイナミックカラーをサポートし、カスタムカラーは代替として使用してください。
2. ナビゲーション [CRITICAL]
2.1 ナビゲーションバー(下部)
トップレベルの移動先が3~5個あるスマートフォン向けの主要なナビゲーションパターンです。
// Compose: Navigation Bar
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = {
Icon(
imageVector = if (selectedItem == index) item.filledIcon else item.outlinedIcon,
contentDescription = item.label
)
},
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
ルール:
- R2.1:コンパクト画面でトップレベルの移動先が3~5個ある場合は、ナビゲーションバーを使用してください。3個未満または5個を超える場合は使用しないでください。
- R2.2:ナビゲーションバーの項目には、必ずラベルを表示してください。アイコンのみのナビゲーションバーは許可されません。
- R2.3:選択状態には塗りつぶしアイコンを、未選択状態にはアウトラインアイコンを使用してください。
- R2.4:アクティブインジケーターには
secondaryContainerカラーを使用してください。上書きしないでください。
2.2 ナビゲーションレール
ミディアムおよび拡張画面(タブレット、折りたたみ式デバイス、デスクトップ)向けです。
// Compose: Navigation Rail for larger screens
NavigationRail(
header = {
FloatingActionButton(
onClick = { /* primary action */ },
containerColor = MaterialTheme.colorScheme.tertiaryContainer
) {
Icon(Icons.Default.Add, contentDescription = "Create")
}
}
) {
items.forEachIndexed { index, item ->
NavigationRailItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
ルール:
- R2.5:ウィンドウサイズがミディアム(600~839dp)および拡張(840dp 以上)の場合はナビゲーションレールを使用してください。コンパクト画面ではナビゲーションバーと組み合わせてください。
- R2.6:主要なアクション向けの FAB を、レールのヘッダーに任意で配置できます。
- R2.7:レールのラベルは任意ですが、明確さを高めるため表示を推奨します。
2.3 ナビゲーションドロワー
通常は拡張画面で、移動先が5個以上ある場合や、複雑なナビゲーション階層がある場合に使用します。
// Compose: Permanent Navigation Drawer for large screens
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet {
Text("App Name", modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleMedium)
HorizontalDivider()
items.forEach { item ->
NavigationDrawerItem(
label = { Text(item.label) },
selected = item == selectedItem,
onClick = { selectedItem = item },
icon = { Icon(item.icon, contentDescription = null) }
)
}
}
}
) {
Scaffold { /* page content */ }
}
ルール:
- R2.8:コンパクト画面ではモーダルドロワーを、拡張画面では常設ドロワーを使用してください。
- R2.9:ドロワーの項目は、区切り線とセクションヘッダーを使ってグループ化してください。
2.4 予測型戻るジェスチャー
Android 13+ は、アニメーションプレビュー付きの予測型戻るジェスチャーをサポートしています。
// Compose: Predictive back with BackHandler (androidx.activity.compose)
BackHandler(enabled = true) {
// Called when back is confirmed; navigate back in your nav controller
navController.popBackStack()
}
// Compose: Predictive back progress animation using predictiveBackHandler modifier
// (androidx.activity:activity-compose 1.8+)
Modifier.predictiveBackHandler(enabled = true) { progress ->
// progress is a Flow<BackEventCompat> with x, y, swipeEdge, progress (0.0–1.0)
progress.collect { backEvent ->
animationState = backEvent.progress
}
}
<application android:enableOnBackInvokedCallback="true">
ルール:
- R2.10:マニフェストで予測型戻るジェスチャーを有効にします。Compose アプリでは、戻るイベントをインターセプトするために
BackHandler(androidx.activity.composeから)を使用します。View ベースのアプリでは、onBackPressed()をオーバーライドする代わりに、OnBackInvokedCallback(API 33+)またはOnBackPressedCallback(AndroidX)を実装します。 - R2.11:システムの戻るジェスチャーは、ナビゲーションスタック上で前の画面に戻ります。上へ移動するボタン(ツールバーの矢印)は、アプリの階層内で上の階層に移動します。この2つの動作は異なる場合があります。
- R2.12:ユーザーが未保存の入力をしている場合を除き、「よろしいですか?」ダイアログを表示するためにシステムの戻る操作をインターセプトしてはいけません。
- R2.13:システムが提供する戻るプレビューアニメーションを抑制してはいけません。カスタムの入場/退出トランジションを実装する場合は、
BackEventCompat.progress(0.0~1.0)を使って補間し、BackEventCompat.swipeEdge(EDGE_LEFT/EDGE_RIGHT)を考慮します。これにより、システムアニメーションに合わせて、退出する画面が縮小し、ジェスチャーを開始した端に向かって移動します。 - R2.14:記憶よりも認識しやすさを優先します。各画面にラベルを付け、選択状態を表示し、戻るスタックのコンテキストを保持して、ユーザーがナビゲーションのたびに現在の場所を思い出す必要がないようにします。
// Compose: drive a custom animation from predictive back progress
Modifier.predictiveBackHandler(enabled = true) { progress ->
progress.collect { backEvent ->
// backEvent.progress: 0.0 (gesture start) → 1.0 (committed)
// backEvent.swipeEdge: BackEventCompat.EDGE_LEFT or EDGE_RIGHT
exitScale = 1f - (backEvent.progress * 0.1f)
exitOffsetX = if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) -backEvent.progress * 32.dp.toPx() else backEvent.progress * 32.dp.toPx()
}
}
2.5 ナビゲーションコンポーネントの選択
| 画面サイズ | 3~5 個の遷移先 | 5 個以上の遷移先 |
|---|---|---|
| コンパクト(< 600dp) | ナビゲーションバー | モーダルドロワー + ナビゲーションバー |
| ミディアム(600~839dp) | ナビゲーションレール | モーダルドロワー + ナビゲーションレール |
| エクスパンデッド(840dp+) | ナビゲーションレール | 常設ドロワー |
3. レイアウトとレスポンシブ対応 [HIGH]
3.1 ウィンドウサイズクラス
生のピクセルブレークポイントではなく、ウィンドウサイズクラスを使ってアダプティブなレイアウトを実現します。
// Compose: Window size classes
val windowSizeClass = calculateWindowSizeClass(this)
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> CompactLayout()
WindowWidthSizeClass.Medium -> MediumLayout()
WindowWidthSizeClass.Expanded -> ExpandedLayout()
}
| クラス | 幅 | 代表的なデバイス | 列数 |
|---|---|---|---|
| コンパクト | < 600dp | 縦向きのスマートフォン | 4 |
| ミディアム | 600~839dp | 縦向きのタブレット、折りたたみ式デバイス | 8 |
| エクスパンデッド | 840dp+ | 横向きのタブレット、デスクトップ | 12 |
ルール:
- R3.1:レスポンシブレイアウトを決定する際は、常に
material3-window-size-classのWindowSizeClassを使用します。 - R3.2:固定のピクセルブレークポイントは使用しません。デバイスのカテゴリは流動的です。
- R3.3:3種類すべての幅サイズクラスに対応します。最低でもコンパクトとエクスパンデッドに対応します。
3.2 Material グリッド
標準的な Material グリッドのマージンとガターを適用します。
| サイズクラス | マージン | ガター | 列数 |
|---|---|---|---|
| コンパクト | 16dp | 8dp | 4 |
| ミディアム | 24dp | 16dp | 8 |
| エクスパンデッド | 24dp | 24dp | 12 |
ルール:
- R3.4:エクスパンデッド画面では、コンテンツを画面幅いっぱいに広げないようにします。最大コンテンツ幅を約 840dp にするか、リストと詳細のレイアウトを使用します。
- R3.5:グリッド仕様に沿った、一貫性のある水平マージンを適用します。
3.3 エッジツーエッジ表示
Android 15+ では、エッジツーエッジ表示が必須です。すべてのアプリでシステムバーの背後まで描画する必要があります。
// Compose: Edge-to-edge setup
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
Scaffold(
modifier = Modifier.fillMaxSize(),
// Scaffold handles insets for top/bottom bars automatically
) { innerPadding ->
Content(modifier = Modifier.padding(innerPadding))
}
}
}
}
ルール:
- R3.6:
setContentの前にenableEdgeToEdge()を呼び出します。ステータスバーとナビゲーションバーの両方の背後まで描画します。 - R3.7:
WindowInsetsを使って、システムバーにコンテンツが重ならないようにパディングを設定します。Scaffoldはトップバーとボトムバーのコンテンツに対して、この処理を自動で行います。 - R3.8:スクロール可能なコンテンツは、リストの上下に適切なインセットのパディングを設け、透明なシステムバーの背後までスクロールできるようにします。
3.4 折りたたみ式デバイスのサポート
// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
.windowLayoutInfo(context)
.collectAsState(initial = WindowLayoutInfo(emptyList()))
ルール:
- R3.9:ヒンジ/折り目の位置を検出し、重要なコンテンツが折り目をまたがないようにします。
- R3.10:折りたたみ式デバイスに対応したレイアウトには、Material3 アダプティブライブラリの
ListDetailPaneScaffoldまたはSupportingPaneScaffoldを使用します。
4. タイポグラフィ [HIGH]
4.1 Material の文字サイズスケール
| 役割 | デフォルトサイズ | デフォルトのウェイト | 用途 |
|---|---|---|---|
| displayLarge | 57sp | 400 | ヒーローテキスト、オンボーディング |
| displayMedium | 45sp | 400 | 大きな機能紹介テキスト |
| displaySmall | 36sp | 400 | 強調表示 |
| headlineLarge | 32sp | 400 | 画面タイトル |
| headlineMedium | 28sp | 400 | セクション見出し |
| headlineSmall | 24sp | 400 | カードタイトル |
| titleLarge | 22sp | 400 | トップアプリバーのタイトル |
| titleMedium | 16sp | 500 | タブ、ナビゲーション |
| titleSmall | 14sp | 500 | サブタイトル |
| bodyLarge | 16sp | 400 | 主要な本文 |
| bodyMedium | 14sp | 400 | 補助的な本文 |
| bodySmall | 12sp | 400 | キャプション |
| labelLarge | 14sp | 500 | ボタン、強調ラベル |
| labelMedium | 12sp | 500 | チップ、小さめのラベル |
| labelSmall | 11sp | 500 | タイムスタンプ、注釈 |
// Compose: Custom typography
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily(Font(R.font.brand_regular)),
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily(Font(R.font.brand_regular)),
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
// ... define all 15 roles
)
ルール:
- R4.1:ユーザーのフォントスケーリング設定に対応するため、テキストサイズには必ず
sp単位を使用してください。 - R4.2:本文のテキストは12sp未満にしないでください。ラベルは最小11spまで使用できます。
- R4.3:サイズをハードコードせず、
MaterialTheme.typographyのタイポグラフィロールを参照してください。 - R4.4:ダイナミックタイプのスケーリングに対応してください。フォントスケール200%でテストし、テキストが切れたり重なったりしないようにしてください。
- R4.5:読みやすさのため、行の高さはフォントサイズの約1.2~1.5倍にしてください。
5. コンポーネント [HIGH]
5.1 フローティングアクションボタン(FAB)
FAB は、画面上で最も重要な単一のアクションを表します。
// Compose: FAB variants
// Standard FAB
FloatingActionButton(onClick = { /* action */ }) {
Icon(Icons.Default.Add, contentDescription = "Create new item")
}
// Extended FAB (with label - preferred for clarity)
ExtendedFloatingActionButton(
onClick = { /* action */ },
icon = { Icon(Icons.Default.Edit, contentDescription = null) },
text = { Text("Compose") }
)
// Large FAB
LargeFloatingActionButton(onClick = { /* action */ }) {
Icon(Icons.Default.Add, contentDescription = "Create", modifier = Modifier.size(36.dp))
}
ルール:
- R5.1:1画面につきFABは最大1つにしてください。FAB は主要なアクションを表します。
- R5.2:FAB は画面の右下に配置してください。ナビゲーションバーがある画面では、その上に浮かせます。
- R5.3:デフォルトでは、FAB に
primaryContainerの色を使用してください。セカンダリ画面ではtertiaryContainerを使用してください。 - R5.4:わかりやすさのため、ラベル付きの
ExtendedFloatingActionButtonを優先してください。必要に応じて、スクロール時にアイコンのみの表示に切り替えます。
5.2 トップアプリバー
// Compose: Top app bar variants
// Small (default)
TopAppBar(
title = { Text("Page Title") },
navigationIcon = {
IconButton(onClick = { /* navigate up */ }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* search */ }) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
}
)
// Medium — expands title area
MediumTopAppBar(
title = { Text("Section Title") },
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
)
// Large — for prominent titles
LargeTopAppBar(
title = { Text("Screen Title") },
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
)
ルール:
- R5.5:ほとんどの画面では小さい
TopAppBarを使用してください。目立たせたいセクションや画面のタイトルには、MediumTopAppBarまたはLargeTopAppBarを使用してください。 - R5.6:コンテンツのスクロールに合わせて折りたたまれたり展開したりするよう、スクロール動作をアプリバーに連携させてください。
- R5.7:アクションアイコンは2~3個までにしてください。追加のアクションはその他メニューにまとめます。
5.3 ボトムシート
// Compose: Modal bottom sheet
ModalBottomSheet(
onDismissRequest = { showSheet = false },
sheetState = rememberModalBottomSheetState()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Sheet Title", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
// Sheet content
}
}
ルール:
- R5.8:重要度の低い補足コンテンツにはモーダルボトムシートを使用してください。常に表示するコンテンツには標準のボトムシートを使用してください。
- R5.9:見つけやすくするため、ボトムシートにはドラッグハンドルを表示してください。
- R5.10:表示領域を超える可能性がある場合、シートのコンテンツをスクロール可能にしてください。
5.4 ダイアログ
// Compose: Alert dialog
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Discard draft?") },
text = { Text("Your unsaved changes will be lost.") },
confirmButton = {
TextButton(onClick = { /* confirm */ }) { Text("Discard") }
},
dismissButton = {
TextButton(onClick = { showDialog = false }) { Text("Cancel") }
}
)
ルール:
- R5.11:ダイアログはユーザーの操作を中断します。すぐに注意を向ける必要がある重要な判断に限って使用してください。
- R5.12:確認ボタンには塗りつぶしボタンではなくテキストボタンを使用してください。閉じるボタンは必ず左側に配置します。
- R5.13:ダイアログのタイトルは簡潔な質問または説明文にしてください。本文には背景情報を記載します。
5.5 スナックバー
// Compose: Snackbar with action
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
// trigger snackbar
LaunchedEffect(key) {
val result = snackbarHostState.showSnackbar(
message = "Item archived",
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) { /* undo */ }
}
}
ルール:
- R5.14:短時間の重要度が低いフィードバックにはスナックバーを使用してください。自動的に閉じるため、重要な情報は含めないでください。
- R5.15:スナックバーは画面下部の、ナビゲーションバーの上かつFABの下に表示します。
- R5.16:操作を元に戻せる場合は、「元に戻す」などのアクションを含めてください。アクションは1つに限定します。
5.6 チップ
// Filter Chip
FilterChip(
selected = isSelected,
onClick = { isSelected = !isSelected },
label = { Text("Filter") },
leadingIcon = if (isSelected) {
{ Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(18.dp)) }
} else null
)
// Assist Chip
AssistChip(
onClick = { /* action */ },
label = { Text("Add to calendar") },
leadingIcon = { Icon(Icons.Default.CalendarToday, contentDescription = null) }
)
ルール:
- R5.17:フィルターの切り替えには
FilterChip、スマートな提案にはAssistChip、ユーザーが入力したコンテンツ(タグなど)にはInputChip、動的に生成される提案にはSuggestionChipを使用してください。 - R5.18:チップは縦に積み重ねず、横スクロール可能な行またはフローレイアウトに配置してください。
- R5.19:待機中の状態はすぐに表示してください。アクションをすぐに完了できない場合は、UIを静止したままにせず、インラインの状態変更、進行状況、または別の見える応答で処理中であることを伝えてください。
5.7 コンポーネント選択ガイド
| 必要な機能 | コンポーネント |
|---|---|
| 主要画面のアクション | FAB |
| 簡潔なフィードバック | Snackbar |
| 重要な決定 | Dialog |
| 補足コンテンツ | Bottom Sheet |
| フィルターの切り替え | Filter Chip |
| ユーザーが入力したタグ | Input Chip |
| スマートな提案 | Assist Chip |
| コンテンツのグループ | Card |
| 項目の垂直リスト | ListItem を含む LazyColumn |
| セグメント化された選択肢(2~5 個) | SegmentedButton |
| 2 択の切り替え | Switch |
| リストからの選択 | ラジオボタンまたは公開型ドロップダウンメニュー |
6. アクセシビリティ [CRITICAL]
6.1 TalkBack とコンテンツの説明
// Compose: Accessible components
Icon(
Icons.Default.Favorite,
contentDescription = "Add to favorites" // Descriptive, not "heart icon"
)
// Decorative elements
Icon(
Icons.Default.Star,
contentDescription = null // null for purely decorative
)
// Merge semantics for compound elements
Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
Icon(Icons.Default.Event, contentDescription = null)
Text("March 15, 2026")
}
// Custom actions
Box(modifier = Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction("Archive") { /* archive */ true },
CustomAccessibilityAction("Delete") { /* delete */ true }
)
})
ルール:
- R6.1:すべてのインタラクティブ要素には
contentDescriptionを設定する必要があります(純粋に装飾目的の場合はnull)。 - R6.2:コンテンツの説明では、見た目ではなくアクションや意味を説明します。「ハートのアイコン」ではなく「お気に入りに追加」とします。
- R6.3:関連する要素を 1 つの TalkBack フォーカス単位にまとめるには、
mergeDescendants = trueを使用します(例:アイコン、テキスト、サブタイトルを含むリスト項目)。 - R6.4:スワイプによる閉じる操作や長押し操作を TalkBack ユーザーが利用できるよう、
customActionsを指定します。
6.2 タッチターゲット
// Compose: Ensure minimum touch target
IconButton(onClick = { /* action */ }) {
// IconButton already provides 48dp minimum touch target
Icon(Icons.Default.Close, contentDescription = "Close")
}
// Manual minimum touch target
Box(
modifier = Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.clickable { /* action */ },
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Info, contentDescription = "Info", modifier = Modifier.size(24.dp))
}
ルール:
- R6.5:すべてのインタラクティブ要素には、最小 48x48dp のタッチターゲットが必要です。Material 3 コンポーネントはデフォルトでこれに対応しています。
- R6.6:スペースを節約するためにタッチターゲットを小さくしないでください。視覚要素が小さい場合は、パディングを使ってタッチ可能な領域を広げます。
6.3 色のコントラストと視覚
ルール:
- R6.7:テキストのコントラスト比は、背景に対して通常のテキストでは 4.5:1 以上、大きなテキスト(18sp 以上、または太字で 14sp 以上)では 3:1 以上にする必要があります。
- R6.8:情報を伝える手段として色だけを使用しないでください。アイコン、テキスト、またはパターンを併用します。
- R6.9:太字テキストと高コントラストのアクセシビリティ設定に対応します。
Configuration.fontWeightAdjustment(API 31 以降)を使用してユーザーの太字設定を検出し、それに応じてカスタムフォントのウェイトを調整します。AccessibilityManager.isHighTextContrastEnabled()を使用して高コントラストモードを検出し、よりコントラストの高い色に置き換えます。Material 3 コンポーネントはどちらにも自動で対応しますが、カスタムテキストの描画と色の使用では明示的に有効にする必要があります。
// Detect bold text preference (API 31+)
val fontWeightAdjustment = resources.configuration.fontWeightAdjustment
val isBoldText = fontWeightAdjustment >= 700 // equivalent to FontWeight.Bold.weight
// Detect high contrast mode
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
val isHighContrast = am.isHighTextContrastEnabled
// Compose: use MaterialTheme.typography which respects fontWeightAdjustment automatically
Text(
text = "Label",
style = MaterialTheme.typography.bodyLarge // Adapts to fontWeightAdjustment
)
// For custom colors: provide high-contrast alternative
val labelColor = if (isHighContrast) {
MaterialTheme.colorScheme.onSurface // Strong contrast
} else {
MaterialTheme.colorScheme.onSurfaceVariant // Normal contrast
}
6.4 フォーカスと移動順序
// Compose: Custom focus order
Column {
var focusRequester = remember { FocusRequester() }
TextField(
modifier = Modifier.focusRequester(focusRequester),
value = text,
onValueChange = { text = it }
)
LaunchedEffect(Unit) {
focusRequester.requestFocus() // Auto-focus on screen load
}
}
ルール:
- R6.10:フォーカス順序は論理的な読み上げ順(上から下、開始位置から終了位置)に従う必要があります。デフォルトの順序が不適切な場合を除き、カスタムの
focusOrderは使用しないでください。 - R6.11:画面遷移後やダイアログを閉じた後は、最も論理的な対象要素にフォーカスを移動します。
- R6.12:すべての画面は、TalkBack、Switch Access、外付けキーボードを使用して完全に操作できる必要があります。
6.5 カスタム Canvas ビュー
Canvas にコンテンツを描画するカスタム View サブクラス(グラフ、カスタムピッカー、描画サーフェスなど)は、子ビューがないため、デフォルトでは TalkBack から認識できません。仮想アクセシビリティツリーを定義するには、androidx.customview.widget の ExploreByTouchHelper を使用します。
- R6.13:Canvas に描画するカスタムビューでは、
ExploreByTouchHelperを使用して TalkBack に仮想アクセシビリティツリーを公開する必要があります。getVirtualViewAt()をオーバーライドしてタッチ座標を仮想ビュー ID に対応付け、onPopulateNodeForVirtualView()をオーバーライドして各仮想ノードのテキスト、境界、アクションを指定します。
import androidx.customview.widget.ExploreByTouchHelper
class PieChartView(context: Context) : View(context) {
private val helper = object : ExploreByTouchHelper(this) {
override fun getVirtualViewAt(x: Float, y: Float): Int {
// Return virtual view ID for the slice at (x, y), or INVALID_ID
return sliceIndexAt(x, y)
}
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
slices.indices.forEach { virtualViewIds.add(it) }
}
override fun onPopulateNodeForVirtualView(
virtualViewId: Int,
node: AccessibilityNodeInfoCompat
) {
val slice = slices[virtualViewId]
node.text = "${slice.label}: ${slice.percentage}%"
node.setBoundsInParent(slice.bounds)
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
}
override fun onPerformActionForVirtualView(
virtualViewId: Int, action: Int, arguments: Bundle?
): Boolean {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
onSliceSelected(virtualViewId)
return true
}
return false
}
}
init {
ViewCompat.setAccessibilityDelegate(this, helper)
}
override fun dispatchHoverEvent(event: MotionEvent) =
helper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event)
}
7. ジェスチャーと入力 [MEDIUM]
7.1 システムジェスチャー
ルール:
- R7.1:システムナビゲーションジェスチャーと競合するため、システムジェスチャーのインセット領域(下端 20dp、左右端 24dp)にインタラクティブ要素を配置しないでください。
- R7.2:
WindowInsets.systemGesturesを使用して、ジェスチャーと競合する領域を検出し、回避します。
7.2 一般的なジェスチャーパターン
// Compose: Pull to refresh
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = { viewModel.refresh() }
) {
LazyColumn { /* content */ }
}
// Compose: Swipe to dismiss
SwipeToDismissBox(
state = rememberSwipeToDismissBoxState(),
backgroundContent = {
Box(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.error),
contentAlignment = Alignment.CenterEnd
) {
Icon(Icons.Default.Delete, contentDescription = "Delete",
tint = MaterialTheme.colorScheme.onError)
}
}
) {
ListItem(headlineContent = { Text("Swipeable item") })
}
ルール:
- R7.3:スワイプで閉じる操作は、元に戻せるようにする(元に戻す操作を含むスナックバーを表示する)か、確認を求める必要があります。
- R7.4:アクセシビリティのため、ジェスチャーによるすべての操作には、ジェスチャーを使わない実行方法も用意します。
- R7.5:タップ可能なすべての要素に Material のリップル効果を適用します。Compose の
clickable修飾子には、デフォルトでリップル効果が含まれています。
7.3 長押し
ルール:
- R7.6:コンテキストメニューや複数選択モードには長押しを使用します。機能にアクセスする唯一の方法として長押しを使用してはいけません。
- R7.7:長押し時に
HapticFeedbackType.LongPressによる触覚フィードバックを提供します。
8. 通知[MEDIUM]
8.1 通知チャンネル
// Create notification channel (required for Android 8+)
val channel = NotificationChannel(
"messages",
"Messages",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "New message notifications"
enableLights(true)
lightColor = Color.BLUE
}
notificationManager.createNotificationChannel(channel)
| 重要度 | 動作 | 用途 |
|---|---|---|
| IMPORTANCE_HIGH | サウンド+ヘッズアップ通知 | メッセージ、通話 |
| IMPORTANCE_DEFAULT | サウンド | ソーシャルアップデート、メール |
| IMPORTANCE_LOW | サウンドなし | おすすめ |
| IMPORTANCE_MIN | サイレント、ステータスバーに表示しない | 天気、継続中の通知 |
ルール:
- R8.1:通知の種類ごとに個別の通知チャンネルを作成します。ユーザーはそれぞれを個別に設定できます。
- R8.2:重要度レベルは慎重に選択します。
IMPORTANCE_HIGHを多用すると、ユーザーが通知をすべて無効にする原因になります。 - R8.3:すべての通知に、関連するコンテンツへ移動するタップアクション(PendingIntent)を設定する必要があります。
- R8.4:アクセシビリティのため、通知アイコンに
contentDescriptionを含めます。
8.2 通知のデザイン
ルール:
- R8.5:会話には
MessagingStyleを使用します。送信者名とアバターを含めます。 - R8.6:メッセージ通知に直接返信アクションを追加します。
- R8.7:メッセージ通知に「既読にする」アクションを追加します。
- R8.8:リッチコンテンツには展開可能な通知(
BigTextStyle、BigPictureStyle、InboxStyle)を使用します。 - R8.9:フォアグラウンドサービスの通知には、実行中の操作を正確に記載し、適切な場合は停止アクションを提供します。
9. 権限とプライバシー[HIGH]
9.1 実行時権限
// Compose: Permission request
val permissionState = rememberPermissionState(Manifest.permission.CAMERA)
if (permissionState.status.isGranted) {
CameraPreview()
} else {
Column {
Text("Camera access is needed to scan QR codes.")
Button(onClick = { permissionState.launchPermissionRequest() }) {
Text("Grant Camera Access")
}
}
}
ルール:
- R9.1:権限はアプリの起動時ではなく、必要になった時点で、必要な場面に合わせてリクエストします。
- R9.2:権限をリクエストする前に、必要な理由を必ず説明します(説明画面)。
- R9.3:権限が拒否された場合も適切に処理します。ユーザーをブロックするのではなく、機能を制限して提供します。
- R9.4:実際に使用しない権限はリクエストしません。不要な権限があるアプリは Google Play によって却下されます。
9.2 プライバシーを保護する API
// Photo picker: no permission needed
val pickMedia = rememberLauncherForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let { /* handle selected media */ }
}
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
ルール:
- R9.5:
READ_MEDIA_IMAGESをリクエストする代わりに、フォトピッカー(Android 13 以降)を使用します。権限は不要です。 - R9.6:機能上、正確な位置情報が不可欠でない限り、
ACCESS_COARSE_LOCATION(おおよその位置情報)を使用します。 - R9.7:録画・録音をしない場面では、カメラとマイクに対して一時的な権限を優先します。
- R9.8:カメラまたはマイクを使用中は、プライバシーインジケーターを表示します。
10. システム連携[MEDIUM]
10.1 ウィジェット
// Compose Glance API widget
class TaskWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(16.dp)
) {
Text(
text = "Tasks",
style = TextStyle(fontWeight = FontWeight.Bold,
color = GlanceTheme.colors.onSurface)
)
// Widget content
}
}
}
}
}
ルール:
- R10.1:新しいウィジェットには Glance API を使用します。
GlanceThemeによるダイナミックカラーに対応します。 - R10.2:ウィジェットにはデフォルト設定を用意し、配置後すぐに動作するようにします。
- R10.3:実用的な場合は、複数のウィジェットサイズ(小、中、大)を提供します。
- R10.4:システムウィジェットの形状(
system_app_widget_background_radius)に合わせて角を丸くします。
10.2 アプリのショートカット
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="compose"
android:enabled="true"
android:shortcutShortLabel="@string/compose_short"
android:shortcutLongLabel="@string/compose_long"
android:icon="@drawable/ic_shortcut_compose">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.example.app"
android:targetClass="com.example.app.ComposeActivity" />
</shortcut>
</shortcuts>
ルール:
- R10.5:一般的な操作に使う静的ショートカットを 2~4 個提供します。最近のコンテンツ向けに動的ショートカットにも対応します。
- R10.6:ショートカットアイコンは、円形の背景にシンプルで見分けやすいシルエットを使用します。
- R10.7:アプリアイコンの長押しと、設定 > アプリのショートカット一覧でショートカットをテストします。
10.3 ディープリンクと共有
ルール:
- R10.8:すべての公開コンテンツ URL で、Android App Links(検証済みディープリンク)に対応します。
- R10.9:
ShareCompatまたはIntent.createChooserを使用して共有シートを実装します。タイトル、説明、サムネイルを含むリッチプレビューを提供します。 - R10.10:適切なコンテンツタイプのフィルタリングを使用して、受信した共有インテントを処理します。
デザイン評価チェックリスト
このチェックリストを使って、Android UI の実装を評価します。
テーマと色
- 静的なフォールバックを用意したうえで、ダイナミックカラーを有効化
- すべての色が Material テーマのロールを参照(16 進数のハードコード値を使用しない)
- ライトテーマとダークテーマの両方に対応
- オンカラーが背景色のロールに一致
- Material Theme Builder を使用し、シードからカスタムカラーを生成
ナビゲーション
- 画面サイズと遷移先の数に適したナビゲーションコンポーネントを使用する
- ナビゲーションバーのラベルを常に表示する
- 予測型戻るジェスチャーを有効にし、処理する
- Up と Back の動作が適切である
レイアウト
- 3 種類すべてのウィンドウサイズクラスをサポートする
- 適切にインセットを処理してエッジツーエッジ表示にする
- 大画面でコンテンツを全幅に表示しない
- 折りたたみ式デバイスのヒンジ領域を考慮する
タイポグラフィ
- すべてのテキストに sp 単位を使用する
- すべてのテキストで MaterialTheme.typography のロールを参照する
- フォントスケール 200%でクリッピングが発生しないことをテストする
- 本文は最低 12sp、ラベルは 11sp にする
コンポーネント
- 1 画面につき FAB は最大 1 つにする
- トップアプリバーをスクロール動作に連動させる
- スナックバーは重要度の低いフィードバックにのみ使用する
- ダイアログは重大な割り込みに限定する
アクセシビリティ
- すべてのインタラクティブ要素に contentDescription を設定する
- すべてのタッチターゲットを 48dp 以上にする
- テキストの色のコントラスト比を 4.5:1 以上にする
- 色だけで情報を伝えない
- TalkBack による全項目の読み上げをテストする
- Switch Access とキーボードナビゲーションが機能する
ジェスチャー
- システムジェスチャー領域にインタラクティブ要素を配置しない
- すべてのジェスチャー操作に、ジェスチャー以外の代替手段を用意する
- スワイプによる閉じる操作を取り消し可能にする
通知
- 通知の種類ごとに個別のチャンネルを用意する
- 適切な重要度を設定する
- タップすると関連するコンテンツに移動する
権限
- 起動時ではなく、必要な状況で権限をリクエストする
- 権限リクエストの前に理由を説明する
- 権限が拒否された場合も適切に機能を縮退させる
- メディア権限の代わりに Photo Picker を使用する
システム統合
- ウィジェットで Glance API とダイナミックカラーを使用する
- よく使う操作にアプリのショートカットを用意する
- 公開コンテンツへのディープリンクを処理する
避けるべきパターン
| 避けるべきパターン | 問題点 | 適切な方法 |
|---|---|---|
| 色の 16 進数値をハードコードする | ダイナミックカラーとダークテーマに対応できなくなります | MaterialTheme.colorScheme のロールを使用する |
テキストサイズに dp を使用する | ユーザーのフォントスケーリングが反映されません | sp 単位を使用する |
| 独自のボトムナビゲーションバーを使用する | プラットフォームとの一貫性がなくなります | Material の NavigationBar を使用する |
| ラベルのないナビゲーションバー | Material のガイドラインに違反します | 常にラベルを表示する |
| 重要度の低い情報にダイアログを使用する | 不必要にユーザーの操作を中断します | スナックバーまたはボトムシートを使用する |
| セカンダリアクションに FAB を使用する | 主な操作の目立ちやすさが損なわれます | FAB は主な操作にのみ 1 つ使用する |
onBackPressed() のオーバーライド | 非推奨であり、予測型戻る操作が機能しなくなります | 予測型戻る操作をサポートするには、BackHandler(Compose)または OnBackInvokedCallback(View ベース)を使用する |
| 48dp 未満のタッチターゲット | アクセシビリティ違反です | 最低 48x48dp を確保する |
| 起動時の権限リクエスト | 理由がわからないため、ユーザーに拒否されます | 理由を説明し、必要な状況でリクエストする |
| ダークテーマに純粋な黒(#000000)を使用する | 目に負担がかかり、Material 3 に準拠しません | Material のサーフェスカラーのロールを使用する |
| アイコンのみのナビゲーションバー | ユーザーが遷移先を識別できません | 常にテキストラベルを含める |
| タブレットでコンテンツを全幅表示する | スペースが無駄になり、読みやすさが損なわれます | 最大幅を設定するか、リストと詳細のレイアウトを使用する |
写真に READ_EXTERNAL_STORAGE を使用する | Android 13 以降では不要です | Photo Picker API を使用する |
| 権限の拒否時に UI をブロックする | ユーザーに不利益を与えます | 機能を適切に縮退させる |
| カラーパレットを手動で選択する | 色調の関係が一貫しなくなります | Material Theme Builder を使用する |