android-design-guidelines
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAndroid 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.
| Role | Usage | On-Role |
|---|---|---|
| Key actions, active states, FAB | |
| Less prominent primary elements | |
| Supporting UI, filter chips | |
| Navigation bar active indicator | |
| Accent, contrast, complementary | |
| Input fields, less prominent accents | |
| Backgrounds, cards, sheets | |
| Decorative elements, dividers | |
| Error states, destructive actions | |
| Error backgrounds | |
| Borders, dividers | — |
| Subtle borders | — |
| Snackbar background | |
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-patternRules:
- R1.4: Every foreground element must use the matching color role for its background (e.g.,
ontext ononPrimarybackground).primary - R1.5: Use and its variants for backgrounds. Never use
surfaceorprimaryas large background areas.secondary - R1.6: Use sparingly for accent and complementary contrast only.
tertiary
Material 3 定义了一套结构化的色彩角色。请按语义而非美学用途使用它们。
| 角色 | 用途 | 对应前景色 |
|---|---|---|
| 关键操作、激活状态、浮动操作按钮(FAB) | |
| 次要的主色调元素 | |
| 辅助UI、筛选芯片 | |
| 导航栏激活指示器 | |
| 强调色、对比色、互补色 | |
| 输入框、次要强调元素 | |
| 背景、卡片、面板 | |
| 装饰元素、分隔线 | |
| 错误状态、破坏性操作 | |
| 错误背景 | |
| 边框、分隔线 | — |
| 细微边框 | — |
| Snackbar 背景 | |
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 color roles which handle this automatically.
surface - 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 color. Do not override this.
secondaryContainer
适用于手机等紧凑屏幕上有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 instead of overriding
OnBackInvokedCallback.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 Size | 3-5 Destinations | 5+ Destinations |
|---|---|---|
| Compact (< 600dp) | Navigation Bar | Modal Drawer + Navigation Bar |
| Medium (600-839dp) | Navigation Rail | Modal Drawer + Navigation Rail |
| Expanded (840dp+) | Navigation Rail | Permanent 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()
}| Class | Width | Typical Device | Columns |
|---|---|---|---|
| Compact | < 600dp | Phone portrait | 4 |
| Medium | 600-839dp | Tablet portrait, foldable | 8 |
| Expanded | 840dp+ | Tablet landscape, desktop | 12 |
Rules:
- R3.1: Always use from
WindowSizeClassfor responsive layout decisions.material3-window-size-class - 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 Class | Margins | Gutters | Columns |
|---|---|---|---|
| Compact | 16dp | 8dp | 4 |
| Medium | 24dp | 16dp | 8 |
| Expanded | 24dp | 24dp | 12 |
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 网格边距和间距。
| 尺寸类别 | 边距 | 间距 | 列数 |
|---|---|---|---|
| 紧凑 | 16dp | 8dp | 4 |
| 中等 | 24dp | 16dp | 8 |
| 扩展 | 24dp | 24dp | 12 |
规则:
- 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 before
enableEdgeToEdge(). Draw behind both status bar and navigation bar.setContent - R3.7: Use to pad content away from system bars.
WindowInsetshandles this for top bar and bottom bar content automatically.Scaffold - 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 or
ListDetailPaneScaffoldfrom Material3 adaptive library for foldable-aware layouts.SupportingPaneScaffold
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 字体层级
| Role | Default Size | Default Weight | Usage |
|---|---|---|---|
| displayLarge | 57sp | 400 | Hero text, onboarding |
| displayMedium | 45sp | 400 | Large feature text |
| displaySmall | 36sp | 400 | Prominent display |
| headlineLarge | 32sp | 400 | Screen titles |
| headlineMedium | 28sp | 400 | Section headers |
| headlineSmall | 24sp | 400 | Card titles |
| titleLarge | 22sp | 400 | Top app bar title |
| titleMedium | 16sp | 500 | Tabs, navigation |
| titleSmall | 14sp | 500 | Subtitles |
| bodyLarge | 16sp | 400 | Primary body text |
| bodyMedium | 14sp | 400 | Secondary body text |
| bodySmall | 12sp | 400 | Captions |
| labelLarge | 14sp | 500 | Buttons, prominent labels |
| labelMedium | 12sp | 500 | Chips, smaller labels |
| labelSmall | 11sp | 500 | Timestamps, 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 units for text sizes to support user font scaling preferences.
sp - R4.2: Never set text below 12sp for body content. Labels may go to 11sp minimum.
- R4.3: Reference typography roles from , not hardcoded sizes.
MaterialTheme.typography - 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.
| 角色 | 默认尺寸 | 默认字重 | 用途 |
|---|---|---|---|
| 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 | 时间戳、注释 |
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 color by default. Use
primaryContainerfor secondary screens.tertiaryContainer - R5.4: Prefer with a label for clarity. Collapse to icon-only on scroll if needed.
ExtendedFloatingActionButton
浮动操作按钮代表屏幕上最重要的操作。
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 (small) for most screens. Use
TopAppBarorMediumTopAppBarfor prominent section or screen titles.LargeTopAppBar - 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 for toggling filters,
FilterChipfor smart suggestions,AssistChipfor user-entered content (tags),InputChipfor dynamically generated suggestions.SuggestionChip - 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 组件选择指南
| Need | Component |
|---|---|
| Primary screen action | FAB |
| Brief feedback | Snackbar |
| Critical decision | Dialog |
| Supplementary content | Bottom Sheet |
| Toggle filter | Filter Chip |
| User-entered tag | Input Chip |
| Smart suggestion | Assist Chip |
| Content group | Card |
| Vertical list of items | LazyColumn with ListItem |
| Segmented option (2-5) | SegmentedButton |
| Binary toggle | Switch |
| Selection from list | Radio 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 (or
contentDescriptionif purely decorative).null - R6.2: Content descriptions must describe the action or meaning, not the visual appearance. Say "Add to favorites" not "Heart icon."
- R6.3: Use to group related elements into a single TalkBack focus unit (e.g., a list item with icon + text + subtitle).
mergeDescendants = true - R6.4: Provide for swipe-to-dismiss or long-press actions so TalkBack users can access them.
customActions
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: 所有交互元素必须有(纯装饰元素设为null)。
contentDescription - 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 unless the default is incorrect.
focusOrder - 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 to detect and avoid gesture conflict zones.
WindowInsets.systemGestures
规则:
- 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 modifier includes ripple by default.
clickable
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)| Importance | Behavior | Use For |
|---|---|---|
| IMPORTANCE_HIGH | Sound + heads-up | Messages, calls |
| IMPORTANCE_DEFAULT | Sound | Social updates, emails |
| IMPORTANCE_LOW | No sound | Recommendations |
| IMPORTANCE_MIN | Silent, no status bar | Weather, 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 leads users to disable notifications entirely.
IMPORTANCE_HIGH - R8.3: All notifications must have a tap action (PendingIntent) that navigates to relevant content.
- R8.4: Include a in notification icons for accessibility.
contentDescription
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 for conversations. Include sender name and avatar.
MessagingStyle - 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) for rich content.InboxStyle - 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 . No permission needed.
READ_MEDIA_IMAGES - R9.6: Use (approximate) unless precise location is essential for functionality.
ACCESS_COARSE_LOCATION - 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 or
ShareCompat. Provide rich previews with title, description, and thumbnail.Intent.createChooser - 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-Pattern | Why It Is Wrong | Correct Approach |
|---|---|---|
| Hardcoded color hex values | Breaks dynamic color and dark theme | Use |
Using | Ignores user font scaling | Use |
| Custom bottom navigation bar | Inconsistent with platform | Use Material |
| Navigation bar without labels | Violates Material guidelines | Always show labels |
| Dialog for non-critical info | Interrupts user unnecessarily | Use Snackbar or Bottom Sheet |
| FAB for secondary actions | Dilutes primary action prominence | One FAB for the primary action only |
| Deprecated; breaks predictive back | Use |
| Touch targets < 48dp | Accessibility violation | Ensure minimum 48x48dp |
| Permission request at launch | Users deny without context | Request in context with rationale |
| Pure black (#000000) dark theme | Eye strain; not Material 3 | Use Material surface color roles |
| Icon-only navigation bar | Users cannot identify destinations | Always include text labels |
| Full-width content on tablets | Wastes space; poor readability | Max width or list-detail layout |
| Unnecessary since Android 13 | Use Photo Picker API |
| Blocking UI on permission denial | Punishes the user | Graceful degradation |
| Manual color palette selection | Inconsistent tonal relationships | Use Material Theme Builder |
| 反模式 | 错误原因 | 正确做法 |
|---|---|---|
| 硬编码颜色十六进制值 | 破坏动态色彩和暗色主题 | 使用 |
文本尺寸使用 | 忽略用户字体缩放偏好 | 使用 |
| 自定义底部导航栏 | 与平台不一致 | 使用Material |
| 无标签的导航栏 | 违反Material指南 | 始终显示标签 |
| 非关键信息使用对话框 | 不必要地打断用户 | 使用提示条或底部面板 |
| 次要操作使用浮动操作按钮 | 降低主要操作的突出性 | 仅为主要操作使用一个浮动操作按钮 |
重写 | 已弃用;破坏预测性返回 | 使用 |
| 触摸目标<48dp | 违反无障碍要求 | 确保最小48x48dp |
| 启动时请求权限 | 用户无上下文时会拒绝 | 在上下文场景中请求并说明理由 |
| 暗色主题使用纯黑色(#000000) | 眼睛疲劳;不符合Material 3 | 使用Material surface色彩角色 |
| 仅图标导航栏 | 用户无法识别目的地 | 始终包含文本标签 |
| 平板上使用全屏内容 | 浪费空间;可读性差 | 使用最大宽度或列表-详情布局 |
照片使用 | Android 13+无需此权限 | 使用照片选择器API |
| 权限拒绝时阻止UI | 惩罚用户 | 优雅降级 |
| 手动选择色彩调色板 | 色调关系不一致 | 使用Material Theme Builder |