android-design-guidelines

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Android Platform Design Guidelines — Material Design 3

Android 平台设计指南 — Material Design 3

1. Material You & Theming [CRITICAL]

1. Material You 与主题配置 [关键]

1.1 Dynamic Color

1.1 动态色彩

Enable dynamic color derived from the user's wallpaper. Dynamic color is the default on Android 12+ and should be the primary theming strategy.
kotlin
// 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
    )
}
xml
<!-- XML: Dynamic color in themes.xml -->
<style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.DayNight</item>
</style>
Rules:
  • R1.1: Always provide a fallback static color scheme for devices below Android 12.
  • R1.2: Never hardcode color hex values in components. Always reference color roles from the theme.
  • R1.3: Test with at least 3 different wallpapers to verify dynamic color harmony.
启用从用户壁纸提取的动态色彩。动态色彩是 Android 12 及以上版本的默认设置,应作为主要的主题配置策略。
kotlin
// 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
    )
}
xml
<!-- XML: Dynamic color in themes.xml -->
<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: 绝不在组件中硬编码颜色十六进制值,始终引用主题中的色彩角色。
  • R1.3: 至少使用3种不同壁纸测试,验证动态色彩的协调性。

1.2 Color Roles

1.2 色彩角色

Material 3 defines a structured set of color roles. Use them semantically, not aesthetically.
RoleUsageOn-Role
primary
Key actions, active states, FAB
onPrimary
primaryContainer
Less prominent primary elements
onPrimaryContainer
secondary
Supporting UI, filter chips
onSecondary
secondaryContainer
Navigation bar active indicator
onSecondaryContainer
tertiary
Accent, contrast, complementary
onTertiary
tertiaryContainer
Input fields, less prominent accents
onTertiaryContainer
surface
Backgrounds, cards, sheets
onSurface
surfaceVariant
Decorative elements, dividers
onSurfaceVariant
error
Error states, destructive actions
onError
errorContainer
Error backgrounds
onErrorContainer
outline
Borders, dividers
outlineVariant
Subtle borders
inverseSurface
Snackbar background
inverseOnSurface
kotlin
// 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
Rules:
  • R1.4: Every foreground element must use the matching
    on
    color role for its background (e.g.,
    onPrimary
    text on
    primary
    background).
  • R1.5: Use
    surface
    and its variants for backgrounds. Never use
    primary
    or
    secondary
    as large background areas.
  • R1.6: Use
    tertiary
    sparingly for accent and complementary contrast only.
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
Snackbar 背景
inverseOnSurface
kotlin
// 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 Light and Dark Themes

1.3 亮色与暗色主题

Support both light and dark themes. Respect the system setting by default.
kotlin
// Compose: Detect system theme
val darkTheme = isSystemInDarkTheme()
Rules:
  • R1.7: Always support both light and dark themes. Never ship light-only.
  • R1.8: Dark theme surfaces use elevation-based tonal mapping, not pure black (#000000). Use
    surface
    color roles which handle this automatically.
  • R1.9: Provide a manual theme override in app settings (System / Light / Dark).
同时支持亮色和暗色主题,默认遵循系统设置。
kotlin
// Compose: Detect system theme
val darkTheme = isSystemInDarkTheme()
规则:
  • R1.7: 始终同时支持亮色和暗色主题,绝不要只发布亮色版本。
  • R1.8: 暗色主题的表面使用基于海拔的色调映射,而非纯黑色(#000000)。使用
    surface
    色彩角色可自动处理此问题。
  • R1.9: 在应用设置中提供手动主题切换选项(系统/亮色/暗色)。

1.4 Custom Color Seeds

1.4 自定义色彩种子

When branding requires custom colors, provide a seed color and generate tonal palettes using Material Theme Builder.
kotlin
// 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
)
Rules:
  • R1.10: Generate tonal palettes from seed colors using Material Theme Builder. Never manually pick individual tones.
  • R1.11: When using custom colors, still support dynamic color as the default and use custom colors as fallback.

当品牌需要自定义色彩时,提供一个种子色并使用 Material Theme Builder 生成色调调色板。
kotlin
// 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. Navigation [CRITICAL]

2. 导航 [关键]

2.1 Navigation Bar (Bottom)

2.1 底部导航栏

The primary navigation pattern for phones with 3-5 top-level destinations.
kotlin
// 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 }
        )
    }
}
Rules:
  • R2.1: Use Navigation Bar for 3-5 top-level destinations on compact screens. Never use for fewer than 3 or more than 5.
  • R2.2: Always show labels on navigation bar items. Icon-only navigation bars are not permitted.
  • R2.3: Use filled icons for the selected state and outlined icons for unselected states.
  • R2.4: The active indicator uses
    secondaryContainer
    color. Do not override this.
适用于手机等紧凑屏幕上有3-5个顶级目的地的场景。
kotlin
// 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 Navigation Rail

2.2 侧边导航栏

For medium and expanded screens (tablets, foldables, desktop).
kotlin
// 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 }
        )
    }
}
Rules:
  • R2.5: Use Navigation Rail on medium (600-839dp) and expanded (840dp+) window sizes. Pair it with Navigation Bar on compact.
  • R2.6: Optionally include a FAB in the rail header for the primary action.
  • R2.7: Labels are optional on the rail but recommended for clarity.
适用于中等和扩展屏幕(平板、折叠屏、桌面设备)。
kotlin
// 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 Navigation Drawer

2.3 导航抽屉

For 5+ destinations or complex navigation hierarchies, typically on expanded screens.
kotlin
// 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 */ }
}
Rules:
  • R2.8: Use modal drawer on compact screens, permanent drawer on expanded screens.
  • R2.9: Group drawer items into sections with dividers and section headers.
适用于5个以上目的地或复杂导航层级,通常用于扩展屏幕。
kotlin
// 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 Predictive Back Gesture

2.4 预测性返回手势

Android 13+ supports predictive back with an animation preview.
kotlin
// Compose: Predictive back handling
val predictiveBackHandler = remember { PredictiveBackHandler(enabled = true) { progress ->
    // Animate based on progress (0.0 to 1.0)
}}
xml
<!-- AndroidManifest.xml: opt in to predictive back -->
<application android:enableOnBackInvokedCallback="true">
Rules:
  • R2.10: Opt in to predictive back in the manifest. Handle
    OnBackInvokedCallback
    instead of overriding
    onBackPressed()
    .
  • R2.11: The system back gesture navigates back in the navigation stack. The Up button (toolbar arrow) navigates up in the app hierarchy. These may differ.
  • R2.12: Never intercept system back to show "are you sure?" dialogs unless there is unsaved user input.
Android 13+ 支持带动画预览的预测性返回功能。
kotlin
// Compose: Predictive back handling
val predictiveBackHandler = remember { PredictiveBackHandler(enabled = true) { progress ->
    // Animate based on progress (0.0 to 1.0)
}}
xml
<!-- AndroidManifest.xml: opt in to predictive back -->
<application android:enableOnBackInvokedCallback="true">
规则:
  • R2.10: 在清单文件中启用预测性返回,使用
    OnBackInvokedCallback
    而非重写
    onBackPressed()
  • R2.11: 系统返回手势在导航栈中后退,向上按钮(工具栏箭头)在应用层级中向上导航,两者行为可能不同。
  • R2.12: 除非存在未保存的用户输入,否则绝不要拦截系统返回以显示“确定要退出吗?”对话框。

2.5 Navigation Component Selection

2.5 导航组件选择

Screen Size3-5 Destinations5+ Destinations
Compact (< 600dp)Navigation BarModal Drawer + Navigation Bar
Medium (600-839dp)Navigation RailModal Drawer + Navigation Rail
Expanded (840dp+)Navigation RailPermanent Drawer

屏幕尺寸3-5个目的地5个以上目的地
紧凑(< 600dp)底部导航栏模态抽屉 + 底部导航栏
中等(600-839dp)侧边导航栏模态抽屉 + 侧边导航栏
扩展(840dp+)侧边导航栏固定抽屉

3. Layout & Responsive [HIGH]

3. 布局与响应式设计 [重要]

3.1 Window Size Classes

3.1 窗口尺寸类别

Use window size classes for adaptive layouts, not raw pixel breakpoints.
kotlin
// Compose: Window size classes
val windowSizeClass = calculateWindowSizeClass(this)
when (windowSizeClass.widthSizeClass) {
    WindowWidthSizeClass.Compact -> CompactLayout()
    WindowWidthSizeClass.Medium -> MediumLayout()
    WindowWidthSizeClass.Expanded -> ExpandedLayout()
}
ClassWidthTypical DeviceColumns
Compact< 600dpPhone portrait4
Medium600-839dpTablet portrait, foldable8
Expanded840dp+Tablet landscape, desktop12
Rules:
  • R3.1: Always use
    WindowSizeClass
    from
    material3-window-size-class
    for responsive layout decisions.
  • R3.2: Never use fixed pixel breakpoints. Device categories are fluid.
  • R3.3: Support all three width size classes. At minimum, compact and expanded.
使用窗口尺寸类别而非原始像素断点实现自适应布局。
kotlin
// 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.2 Material Grid

3.2 Material 网格

Apply canonical Material grid margins and gutters.
Size ClassMarginsGuttersColumns
Compact16dp8dp4
Medium24dp16dp8
Expanded24dp24dp12
Rules:
  • R3.4: Content should not span the full width on expanded screens. Use a max content width of ~840dp or list-detail layout.
  • R3.5: Apply consistent horizontal margins matching the grid spec.
应用标准的 Material 网格边距和间距。
尺寸类别边距间距列数
紧凑16dp8dp4
中等24dp16dp8
扩展24dp24dp12
规则:
  • R3.4: 扩展屏幕上的内容不应占满整个宽度,使用约840dp的最大内容宽度或列表-详情布局。
  • R3.5: 应用与网格规范一致的水平边距。

3.3 Edge-to-Edge Display

3.3 全屏显示

Android 15+ enforces edge-to-edge. All apps should draw behind system bars.
kotlin
// 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))
            }
        }
    }
}
Rules:
  • R3.6: Call
    enableEdgeToEdge()
    before
    setContent
    . Draw behind both status bar and navigation bar.
  • R3.7: Use
    WindowInsets
    to pad content away from system bars.
    Scaffold
    handles this for top bar and bottom bar content automatically.
  • R3.8: Scrollable content should scroll behind transparent system bars with appropriate inset padding at the top and bottom of the list.
Android 15+ 强制要求全屏显示,所有应用应在系统栏后方绘制内容。
kotlin
// 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 Foldable Device Support

3.4 折叠屏支持

kotlin
// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
    .windowLayoutInfo(context)
    .collectAsState(initial = WindowLayoutInfo(emptyList()))
Rules:
  • R3.9: Detect hinge/fold position and avoid placing critical content across the fold.
  • R3.10: Use
    ListDetailPaneScaffold
    or
    SupportingPaneScaffold
    from Material3 adaptive library for foldable-aware layouts.

kotlin
// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
    .windowLayoutInfo(context)
    .collectAsState(initial = WindowLayoutInfo(emptyList()))
规则:
  • R3.9: 检测铰链/折叠位置,避免将关键内容放置在折叠区域。
  • R3.10: 使用 Material3 自适应库中的
    ListDetailPaneScaffold
    SupportingPaneScaffold
    实现支持折叠屏的布局。

4. Typography [HIGH]

4. 排版 [重要]

4.1 Material Type Scale

4.1 Material 字体层级

RoleDefault SizeDefault WeightUsage
displayLarge57sp400Hero text, onboarding
displayMedium45sp400Large feature text
displaySmall36sp400Prominent display
headlineLarge32sp400Screen titles
headlineMedium28sp400Section headers
headlineSmall24sp400Card titles
titleLarge22sp400Top app bar title
titleMedium16sp500Tabs, navigation
titleSmall14sp500Subtitles
bodyLarge16sp400Primary body text
bodyMedium14sp400Secondary body text
bodySmall12sp400Captions
labelLarge14sp500Buttons, prominent labels
labelMedium12sp500Chips, smaller labels
labelSmall11sp500Timestamps, annotations
kotlin
// 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
)
Rules:
  • R4.1: Always use
    sp
    units for text sizes to support user font scaling preferences.
  • R4.2: Never set text below 12sp for body content. Labels may go to 11sp minimum.
  • R4.3: Reference typography roles from
    MaterialTheme.typography
    , not hardcoded sizes.
  • R4.4: Support dynamic type scaling. Test at 200% font scale. Ensure no text is clipped or overlapping.
  • R4.5: Line height should be approximately 1.2-1.5x the font size for readability.

角色默认尺寸默认字重用途
displayLarge57sp400标题文本、引导页
displayMedium45sp400大型功能文本
displaySmall36sp400突出显示文本
headlineLarge32sp400屏幕标题
headlineMedium28sp400章节标题
headlineSmall24sp400卡片标题
titleLarge22sp400顶部应用栏标题
titleMedium16sp500标签页、导航
titleSmall14sp500副标题
bodyLarge16sp400主要正文文本
bodyMedium14sp400次要正文文本
bodySmall12sp400说明文字
labelLarge14sp500按钮、突出标签
labelMedium12sp500芯片、小型标签
labelSmall11sp500时间戳、注释
kotlin
// 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. Components [HIGH]

5. 组件 [重要]

5.1 Floating Action Button (FAB)

5.1 浮动操作按钮(FAB)

The FAB represents the single most important action on a screen.
kotlin
// 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))
}
Rules:
  • R5.1: Use at most one FAB per screen. It represents the primary action.
  • R5.2: Place the FAB at the bottom-end of the screen. On screens with a Navigation Bar, the FAB floats above it.
  • R5.3: The FAB should use
    primaryContainer
    color by default. Use
    tertiaryContainer
    for secondary screens.
  • R5.4: Prefer
    ExtendedFloatingActionButton
    with a label for clarity. Collapse to icon-only on scroll if needed.
浮动操作按钮代表屏幕上最重要的操作。
kotlin
// 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: 每个屏幕最多使用一个浮动操作按钮,代表主要操作。
  • R5.2: 将浮动操作按钮放置在屏幕右下角,带有底部导航栏的屏幕中,浮动操作按钮悬浮在导航栏上方。
  • R5.3: 浮动操作按钮默认使用
    primaryContainer
    色彩,次要屏幕使用
    tertiaryContainer
  • R5.4: 优先选择带标签的
    ExtendedFloatingActionButton
    以提升清晰度,可在滚动时折叠为仅图标样式。

5.2 Top App Bar

5.2 顶部应用栏

kotlin
// 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()
)
Rules:
  • R5.5: Use
    TopAppBar
    (small) for most screens. Use
    MediumTopAppBar
    or
    LargeTopAppBar
    for prominent section or screen titles.
  • R5.6: Connect scroll behavior to the app bar so it collapses/expands with content scrolling.
  • R5.7: Limit action icons to 2-3. Overflow additional actions into a more menu.
kotlin
// 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 Bottom Sheets

5.3 底部面板

kotlin
// 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
    }
}
Rules:
  • R5.8: Use modal bottom sheets for non-critical supplementary content. Use standard bottom sheets for persistent content.
  • R5.9: Bottom sheets must have a visible drag handle for discoverability.
  • R5.10: Sheet content must be scrollable if it can exceed the visible area.
kotlin
// 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 Dialogs

5.4 对话框

kotlin
// 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") }
    }
)
Rules:
  • R5.11: Dialogs interrupt the user. Use them only for critical decisions requiring immediate attention.
  • R5.12: Confirm button uses a text button, not a filled button. The dismiss button is always on the left.
  • R5.13: Dialog titles should be concise questions or statements. Body text provides context.
kotlin
// 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 Snackbar

5.5 提示条

kotlin
// 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 */ }
    }
}
Rules:
  • R5.14: Use snackbars for brief, non-critical feedback. They auto-dismiss and should not contain critical information.
  • R5.15: Snackbars appear at the bottom of the screen, above the Navigation Bar and below the FAB.
  • R5.16: Include an action (e.g., "Undo") when the operation is reversible. Limit to one action.
kotlin
// 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: 提示条显示在屏幕底部,位于导航栏上方、浮动操作按钮下方。
  • R5.16: 当操作可撤销时,添加操作按钮(如“撤销”),限制为一个操作。

5.6 Chips

5.6 芯片

kotlin
// 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) }
)
Rules:
  • R5.17: Use
    FilterChip
    for toggling filters,
    AssistChip
    for smart suggestions,
    InputChip
    for user-entered content (tags),
    SuggestionChip
    for dynamically generated suggestions.
  • R5.18: Chips should be arranged in a horizontally scrollable row or a flow layout, not stacked vertically.
kotlin
// 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: 芯片应排列在水平可滚动行或流式布局中,不要垂直堆叠。

5.7 Component Selection Guide

5.7 组件选择指南

NeedComponent
Primary screen actionFAB
Brief feedbackSnackbar
Critical decisionDialog
Supplementary contentBottom Sheet
Toggle filterFilter Chip
User-entered tagInput Chip
Smart suggestionAssist Chip
Content groupCard
Vertical list of itemsLazyColumn with ListItem
Segmented option (2-5)SegmentedButton
Binary toggleSwitch
Selection from listRadio buttons or exposed dropdown menu

需求组件
屏幕主要操作浮动操作按钮(FAB)
简短反馈提示条(Snackbar)
关键决策对话框(Dialog)
补充内容底部面板(Bottom Sheet)
切换筛选筛选芯片(Filter Chip)
用户输入标签输入芯片(Input Chip)
智能建议辅助芯片(Assist Chip)
内容分组卡片(Card)
垂直项目列表带ListItem的LazyColumn
分段选项(2-5个)SegmentedButton
二元切换开关(Switch)
列表选择单选按钮或下拉菜单

6. Accessibility [CRITICAL]

6. 无障碍 [关键]

6.1 TalkBack and Content Descriptions

6.1 屏幕阅读器与内容描述

kotlin
// 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 }
    )
})
Rules:
  • R6.1: Every interactive element must have a
    contentDescription
    (or
    null
    if purely decorative).
  • R6.2: Content descriptions must describe the action or meaning, not the visual appearance. Say "Add to favorites" not "Heart icon."
  • R6.3: Use
    mergeDescendants = true
    to group related elements into a single TalkBack focus unit (e.g., a list item with icon + text + subtitle).
  • R6.4: Provide
    customActions
    for swipe-to-dismiss or long-press actions so TalkBack users can access them.
kotlin
// 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: 使用
    mergeDescendants = true
    将相关元素分组为单个屏幕阅读器焦点单元(例如,包含图标+文本+副标题的列表项)。
  • R6.4: 为滑动删除或长按操作提供
    customActions
    ,以便屏幕阅读器用户可以访问这些操作。

6.2 Touch Targets

6.2 触摸目标

kotlin
// 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))
}
Rules:
  • R6.5: All interactive elements must have a minimum touch target of 48x48dp. Material 3 components handle this by default.
  • R6.6: Do not reduce touch targets to save space. Use padding to increase the touchable area if the visual element is smaller.
kotlin
// 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 Color Contrast and Visual

6.3 色彩对比度与视觉

Rules:
  • R6.7: Text contrast ratio must be at least 4.5:1 for normal text and 3:1 for large text (18sp+ or 14sp+ bold) against its background.
  • R6.8: Never use color as the only means of conveying information. Pair with icons, text, or patterns.
  • R6.9: Support "bold text" and "high contrast" accessibility settings.
规则:
  • R6.7: 普通文本与背景的对比度必须至少为4.5:1,大文本(18sp+或14sp+粗体)与背景的对比度至少为3:1。
  • R6.8: 绝不要仅使用颜色传达信息,搭配图标、文本或图案。
  • R6.9: 支持“粗体文本”和“高对比度”无障碍设置。

6.4 Focus and Traversal

6.4 焦点与遍历

kotlin
// 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
    }
}
Rules:
  • R6.10: Focus order must follow a logical reading sequence (top-to-bottom, start-to-end). Avoid custom
    focusOrder
    unless the default is incorrect.
  • R6.11: After navigation or dialog dismissal, move focus to the most logical target element.
  • R6.12: All screens must be fully operable using TalkBack, Switch Access, and external keyboard.

kotlin
// 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: 所有屏幕必须可通过屏幕阅读器、切换控制和外接键盘完全操作。

7. Gestures & Input [MEDIUM]

7. 手势与输入 [中等]

7.1 System Gestures

7.1 系统手势

Rules:
  • R7.1: Never place interactive elements within the system gesture inset zones (bottom 20dp, left/right 24dp edges) as they conflict with system navigation gestures.
  • R7.2: Use
    WindowInsets.systemGestures
    to detect and avoid gesture conflict zones.
规则:
  • R7.1: 绝不要在系统手势插入区域(底部20dp、左右24dp边缘)放置交互元素,避免与系统导航手势冲突。
  • R7.2: 使用
    WindowInsets.systemGestures
    检测并避开手势冲突区域。

7.2 Common Gesture Patterns

7.2 常见手势模式

kotlin
// 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") })
}
Rules:
  • R7.3: All swipe-to-dismiss actions must be undoable (show snackbar with undo) or require confirmation.
  • R7.4: Provide alternative non-gesture ways to trigger all gesture-based actions (for accessibility).
  • R7.5: Apply Material ripple effect on all tappable elements. Compose
    clickable
    modifier includes ripple by default.
kotlin
// 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 Long Press

7.3 长按

Rules:
  • R7.6: Use long press for contextual menus and multi-select mode. Never use it as the only way to access a feature.
  • R7.7: Provide haptic feedback on long press via
    HapticFeedbackType.LongPress
    .

规则:
  • R7.6: 长按用于上下文菜单和多选模式,绝不要将其作为访问功能的唯一方式。
  • R7.7: 长按通过
    HapticFeedbackType.LongPress
    提供触觉反馈。

8. Notifications [MEDIUM]

8. 通知 [中等]

8.1 Notification Channels

8.1 通知渠道

kotlin
// 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)
ImportanceBehaviorUse For
IMPORTANCE_HIGHSound + heads-upMessages, calls
IMPORTANCE_DEFAULTSoundSocial updates, emails
IMPORTANCE_LOWNo soundRecommendations
IMPORTANCE_MINSilent, no status barWeather, ongoing
Rules:
  • R8.1: Create separate notification channels for each distinct notification type. Users can configure each independently.
  • R8.2: Choose importance levels conservatively. Overusing
    IMPORTANCE_HIGH
    leads users to disable notifications entirely.
  • R8.3: All notifications must have a tap action (PendingIntent) that navigates to relevant content.
  • R8.4: Include a
    contentDescription
    in notification icons for accessibility.
kotlin
// 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 Notification Design

8.2 通知设计

Rules:
  • R8.5: Use
    MessagingStyle
    for conversations. Include sender name and avatar.
  • R8.6: Add direct reply actions to messaging notifications.
  • R8.7: Provide a "Mark as read" action on message notifications.
  • R8.8: Use expandable notifications (
    BigTextStyle
    ,
    BigPictureStyle
    ,
    InboxStyle
    ) for rich content.
  • R8.9: Foreground service notifications must accurately describe the ongoing operation and provide a stop action where appropriate.

规则:
  • R8.5: 对话使用
    MessagingStyle
    ,包含发送者名称和头像。
  • R8.6: 消息通知添加直接回复操作。
  • R8.7: 消息通知提供“标记为已读”操作。
  • R8.8: 使用可展开通知(
    BigTextStyle
    BigPictureStyle
    InboxStyle
    )展示丰富内容。
  • R8.9: 前台服务通知必须准确描述正在进行的操作,并在适当情况下提供停止操作。

9. Permissions & Privacy [HIGH]

9. 权限与隐私 [重要]

9.1 Runtime Permissions

9.1 运行时权限

kotlin
// 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")
        }
    }
}
Rules:
  • R9.1: Request permissions in context, at the moment they are needed, not at app launch.
  • R9.2: Always explain why the permission is needed before requesting it (rationale screen).
  • R9.3: Gracefully handle permission denial. Provide degraded functionality rather than blocking the user.
  • R9.4: Never request permissions you do not actively use. Google Play will reject apps with unnecessary permissions.
kotlin
// 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 Privacy-Preserving APIs

9.2 隐私保护API

kotlin
// Photo picker: no permission needed
val pickMedia = rememberLauncherForActivityResult(
    ActivityResultContracts.PickVisualMedia()
) { uri ->
    uri?.let { /* handle selected media */ }
}
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
Rules:
  • R9.5: Use the Photo Picker (Android 13+) instead of requesting
    READ_MEDIA_IMAGES
    . No permission needed.
  • R9.6: Use
    ACCESS_COARSE_LOCATION
    (approximate) unless precise location is essential for functionality.
  • R9.7: Prefer one-time permissions for camera and microphone in non-recording contexts.
  • R9.8: Display a privacy indicator when camera or microphone is actively in use.

kotlin
// Photo picker: no permission needed
val pickMedia = rememberLauncherForActivityResult(
    ActivityResultContracts.PickVisualMedia()
) { uri ->
    uri?.let { /* handle selected media */ }
}
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
规则:
  • R9.5: 使用照片选择器(Android 13+)而非请求
    READ_MEDIA_IMAGES
    权限,无需权限。
  • R9.6: 除非功能需要精确位置,否则使用
    ACCESS_COARSE_LOCATION
    (近似位置)。
  • R9.7: 在非录制场景中,优先为相机和麦克风使用一次性权限。
  • R9.8: 相机或麦克风处于活动状态时显示隐私指示器。

10. System Integration [MEDIUM]

10. 系统集成 [中等]

10.1 Widgets

10.1 小组件

kotlin
// 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
                }
            }
        }
    }
}
Rules:
  • R10.1: Use Glance API for new widgets. Support dynamic color via
    GlanceTheme
    .
  • R10.2: Widgets must have a default configuration and work immediately after placement.
  • R10.3: Provide multiple widget sizes (small, medium, large) where practical.
  • R10.4: Use rounded corners matching the system widget shape (
    system_app_widget_background_radius
    ).
kotlin
// 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 App Shortcuts

10.2 应用快捷方式

xml
<!-- shortcuts.xml -->
<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>
Rules:
  • R10.5: Provide 2-4 static shortcuts for common actions. Support dynamic shortcuts for recent content.
  • R10.6: Shortcut icons should be simple, recognizable silhouettes on a circular background.
  • R10.7: Test shortcuts with long-press on the app icon and in the Settings > Apps shortcut list.
xml
<!-- shortcuts.xml -->
<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 Deep Links and Share

10.3 深度链接与分享

Rules:
  • R10.8: Support Android App Links (verified deep links) for all public content URLs.
  • R10.9: Implement the share sheet with
    ShareCompat
    or
    Intent.createChooser
    . Provide rich previews with title, description, and thumbnail.
  • R10.10: Handle incoming share intents with appropriate content type filtering.

规则:
  • R10.8: 为所有公开内容URL支持Android应用链接(已验证的深度链接)。
  • R10.9: 使用
    ShareCompat
    Intent.createChooser
    实现分享面板,提供包含标题、描述和缩略图的丰富预览。
  • R10.10: 处理传入的分享意图,过滤适当的内容类型。

Design Evaluation Checklist

设计评估检查表

Use this checklist to evaluate Android UI implementations:
使用此检查表评估Android UI实现:

Theme & Color

主题与色彩

  • Dynamic color enabled with static fallback
  • All colors reference Material theme roles (no hardcoded hex)
  • Light and dark themes both supported
  • On-colors match their background color roles
  • Custom colors generated from seed via Material Theme Builder
  • 启用动态色彩并提供静态降级方案
  • 所有颜色引用Material主题角色(无硬编码十六进制值)
  • 同时支持亮色和暗色主题
  • 前景色与背景色彩角色匹配
  • 自定义色彩通过Material Theme Builder从种子色生成

Navigation

导航

  • Correct navigation component for screen size and destination count
  • Navigation bar labels always visible
  • Predictive back gesture opted in and handled
  • Up vs Back behavior correct
  • 根据屏幕尺寸和目的地数量选择正确的导航组件
  • 导航栏标签始终可见
  • 启用并处理预测性返回手势
  • 向上与返回行为正确

Layout

布局

  • All three window size classes supported
  • Edge-to-edge with proper inset handling
  • Content does not span full width on large screens
  • Foldable hinge area respected
  • 支持所有三种窗口尺寸类别
  • 全屏显示并正确处理内边距
  • 大屏幕内容不占满整个宽度
  • 尊重折叠屏铰链区域

Typography

排版

  • All text uses sp units
  • All text references MaterialTheme.typography roles
  • Tested at 200% font scale with no clipping
  • Minimum 12sp body, 11sp labels
  • 所有文本使用sp单位
  • 所有文本引用MaterialTheme.typography角色
  • 在200%字体比例下测试,无文本裁剪
  • 正文最小12sp,标签最小11sp

Components

组件

  • At most one FAB per screen
  • Top app bar connected to scroll behavior
  • Snackbars used for non-critical feedback only
  • Dialogs reserved for critical interruptions
  • 每个屏幕最多一个浮动操作按钮(FAB)
  • 顶部应用栏与滚动行为关联
  • 提示条仅用于非关键反馈
  • 对话框仅用于关键中断

Accessibility

无障碍

  • All interactive elements have contentDescription
  • All touch targets >= 48dp
  • Color contrast >= 4.5:1 for text
  • No information conveyed by color alone
  • Full TalkBack traversal tested
  • Switch Access and keyboard navigation work
  • 所有交互元素有contentDescription
  • 所有触摸目标≥48dp
  • 文本色彩对比度≥4.5:1
  • 不仅通过颜色传达信息
  • 测试完整的屏幕阅读器遍历
  • 切换控制和键盘导航可用

Gestures

手势

  • No interactive elements in system gesture zones
  • All gesture actions have non-gesture alternatives
  • Swipe-to-dismiss is undoable
  • 系统手势区域无交互元素
  • 所有手势操作有非手势替代方式
  • 滑动删除可撤销

Notifications

通知

  • Separate channels for each notification type
  • Appropriate importance levels
  • Tap action navigates to relevant content
  • 每种通知类型有单独渠道
  • 重要性级别合适
  • 点击操作导航到相关内容

Permissions

权限

  • Permissions requested in context, not at launch
  • Rationale shown before permission request
  • Graceful degradation on denial
  • Photo Picker used instead of media permission
  • 在上下文场景中请求权限,而非启动时
  • 请求权限前显示理由
  • 权限拒绝时优雅降级
  • 使用照片选择器替代媒体权限

System Integration

系统集成

  • Widgets use Glance API with dynamic color
  • App shortcuts provided for common actions
  • Deep links handled for public content

  • 小组件使用Glance API并支持动态色彩
  • 为常见操作提供应用快捷方式
  • 处理公开内容的深度链接

Anti-Patterns

反模式

Anti-PatternWhy It Is WrongCorrect Approach
Hardcoded color hex valuesBreaks dynamic color and dark themeUse
MaterialTheme.colorScheme
roles
Using
dp
for text size
Ignores user font scalingUse
sp
units
Custom bottom navigation barInconsistent with platformUse Material
NavigationBar
Navigation bar without labelsViolates Material guidelinesAlways show labels
Dialog for non-critical infoInterrupts user unnecessarilyUse Snackbar or Bottom Sheet
FAB for secondary actionsDilutes primary action prominenceOne FAB for the primary action only
onBackPressed()
override
Deprecated; breaks predictive backUse
OnBackInvokedCallback
Touch targets < 48dpAccessibility violationEnsure minimum 48x48dp
Permission request at launchUsers deny without contextRequest in context with rationale
Pure black (#000000) dark themeEye strain; not Material 3Use Material surface color roles
Icon-only navigation barUsers cannot identify destinationsAlways include text labels
Full-width content on tabletsWastes space; poor readabilityMax width or list-detail layout
READ_EXTERNAL_STORAGE
for photos
Unnecessary since Android 13Use Photo Picker API
Blocking UI on permission denialPunishes the userGraceful degradation
Manual color palette selectionInconsistent tonal relationshipsUse Material Theme Builder
反模式错误原因正确做法
硬编码颜色十六进制值破坏动态色彩和暗色主题使用
MaterialTheme.colorScheme
角色
文本尺寸使用
dp
单位
忽略用户字体缩放偏好使用
sp
单位
自定义底部导航栏与平台不一致使用Material
NavigationBar
无标签的导航栏违反Material指南始终显示标签
非关键信息使用对话框不必要地打断用户使用提示条或底部面板
次要操作使用浮动操作按钮降低主要操作的突出性仅为主要操作使用一个浮动操作按钮
重写
onBackPressed()
已弃用;破坏预测性返回使用
OnBackInvokedCallback
触摸目标<48dp违反无障碍要求确保最小48x48dp
启动时请求权限用户无上下文时会拒绝在上下文场景中请求并说明理由
暗色主题使用纯黑色(#000000)眼睛疲劳;不符合Material 3使用Material surface色彩角色
仅图标导航栏用户无法识别目的地始终包含文本标签
平板上使用全屏内容浪费空间;可读性差使用最大宽度或列表-详情布局
照片使用
READ_EXTERNAL_STORAGE
权限
Android 13+无需此权限使用照片选择器API
权限拒绝时阻止UI惩罚用户优雅降级
手动选择色彩调色板色调关系不一致使用Material Theme Builder