mcpField
A
android-design-guidelines

android-design-guidelines

github.com/ehmoデザイン・UI6311.7k 回インストール32 週間前に更新
リポジトリ

android-design-guidelines について

Material Design 3に基づく、Android UIの設計・実装向けスキルです。ダイナミックカラー、ナビゲーション、レスポンシブレイアウト、文字、折りたたみ端末への対応を扱います。

主な機能

  1. 01ダイナミックカラーとテーマロールのルール
  2. 02画面サイズに応じたナビゲーション選択
  3. 03ウィンドウサイズクラスによるレイアウト指針
  4. 04エッジツーエッジとシステムバーのインセット指針
  5. 05折りたたみレイアウトの推奨事項

活用シーン

  1. 01Androidアプリのダイナミックテーマ設定
  2. 02スマートフォン、タブレット、大画面のナビゲーション設計
  3. 03ウィンドウサイズに応じたレイアウト設計
  4. 04折りたたみ端末のヒンジ周辺の配置確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ehmo/platform-design-skills --skill android-design-guidelines

インストール後、Claude Code で/android-design-guidelinesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

AGENTS.md
metadata.json
rules/
_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主要なアクション、アクティブ状態、FABonPrimary
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 グリッドのマージンとガターを適用します。

サイズクラスマージンガター列数
コンパクト16dp8dp4
ミディアム24dp16dp8
エクスパンデッド24dp24dp12

ルール:

  • 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 の文字サイズスケール
役割デフォルトサイズデフォルトのウェイト用途
displayLarge57sp400ヒーローテキスト、オンボーディング
displayMedium45sp400大きな機能紹介テキスト
displaySmall36sp400強調表示
headlineLarge32sp400画面タイトル
headlineMedium28sp400セクション見出し
headlineSmall24sp400カードタイトル
titleLarge22sp400トップアプリバーのタイトル
titleMedium16sp500タブ、ナビゲーション
titleSmall14sp500サブタイトル
bodyLarge16sp400主要な本文
bodyMedium14sp400補助的な本文
bodySmall12sp400キャプション
labelLarge14sp500ボタン、強調ラベル
labelMedium12sp500チップ、小さめのラベル
labelSmall11sp500タイムスタンプ、注釈
// 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 を使用する

コメント