关于 android-design-guidelines
面向 Android 界面设计与实现的 Material Design 3 指南 Skill,涵盖动态配色、导航、响应式布局、排版及折叠设备适配。
主要功能
- 01动态颜色与主题角色规范
- 02按屏幕尺寸选择导航组件
- 03窗口尺寸类响应式布局建议
- 04边到边显示与系统栏内边距指导
- 05折叠设备布局建议
适用场景
- 01为 Android 应用配置动态主题
- 02规划手机、平板和大屏导航
- 03设计适配不同窗口尺寸的布局
- 04检查折叠设备铰链区域的内容放置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ehmo/ platform-design-skills --skill android-design-guidelines安装后在 Claude Code 中输入/android-design-guidelines确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonrules/_sections.md详细介绍
Android 平台设计指南 — Material Design 3
1. Material You 与主题 [CRITICAL]
1.1 动态颜色
启用根据用户壁纸生成的动态颜色。动态颜色是 Android 12+ 的默认选项,并应作为首选主题策略。
// Compose: Dynamic color theme
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}
<style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">
<item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.DayNight</item>
</style>
规则:
- R1.1:始终为 Android 12 以下的设备提供静态颜色方案作为回退。
- R1.2:绝不要在组件中硬编码颜色的十六进制值。始终引用主题中的颜色角色。
- R1.3:至少使用 3 种不同的壁纸进行测试,以验证动态颜色的协调性。
1.2 颜色角色
Material 3 定义了一套结构化的颜色角色。请按语义而非审美用途使用它们。
| 角色 | 用途 | 对应角色 |
|---|---|---|
primary | 关键操作、活动状态、FAB | onPrimary |
primaryContainer | 不那么醒目的主要元素 | onPrimaryContainer |
secondary | 辅助 UI、筛选芯片 | onSecondary |
secondaryContainer | 导航栏活动指示器 | onSecondaryContainer |
tertiary | 点缀、对比、互补 | onTertiary |
tertiaryContainer | 输入框、不那么醒目的点缀 | onTertiaryContainer |
surface | 背景、卡片、底部面板 | onSurface |
surfaceVariant | 装饰元素、分隔线 | onSurfaceVariant |
error | 错误状态、破坏性操作 | onError |
errorContainer | 错误背景 | onErrorContainer |
outline | 边框、分隔线 | — |
outlineVariant | 细微边框 | — |
inverseSurface | Snackbar 背景 | inverseOnSurface |
// Correct: semantic color roles
Text(
text = "Error message",
color = MaterialTheme.colorScheme.error
)
Surface(color = MaterialTheme.colorScheme.errorContainer) {
Text(text = "Error detail", color = MaterialTheme.colorScheme.onErrorContainer)
}
// WRONG: hardcoded colors
Text(text = "Error", color = Color(0xFFB00020)) // Anti-pattern
规则:
- R1.4:每个前景元素都必须使用与其背景相匹配的
on颜色角色(例如,在primary背景上使用onPrimary文字)。 - R1.5:使用
surface及其变体作为背景。绝不要将primary或secondary用作大面积背景。 - R1.6:仅少量使用
tertiary来点缀和提供互补对比。
1.3 浅色和深色主题
同时支持浅色和深色主题。默认遵循系统设置。
// Compose: Detect system theme
val darkTheme = isSystemInDarkTheme()
规则:
- R1.7:始终支持浅色和深色主题。绝不要仅发布浅色主题。
- R1.8:深色主题表面使用基于海拔的色调映射,而非纯黑色(#000000)。使用会自动处理此设置的
surface颜色角色。 - R1.9:在应用设置中提供手动覆盖主题的选项(System / Light / Dark)。
1.4 自定义颜色种子
品牌要求使用自定义颜色时,请提供种子颜色,并使用 Material Theme Builder 生成色调调色板。
// Custom color scheme with brand seed
private val BrandLightColorScheme = lightColorScheme(
primary = Color(0xFF1B6D2F),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFA4F6A8),
onPrimaryContainer = Color(0xFF002107),
// ... generate full palette from seed
)
规则:
- R1.10:使用 Material Theme Builder 从种子颜色生成色调调色板。绝不要手动挑选单个色调。
- R1.11:使用自定义颜色时,仍须默认支持动态颜色,并将自定义颜色用作回退。
2. 导航 [CRITICAL]
2.1 导航栏(底部)
适用于具有 3-5 个顶层目的地的手机的主要导航模式。
// Compose: Navigation Bar
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = {
Icon(
imageVector = if (selectedItem == index) item.filledIcon else item.outlinedIcon,
contentDescription = item.label
)
},
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
规则:
- R2.1:在紧凑屏幕上,有 3-5 个顶层目的地时使用 Navigation Bar。少于 3 个或多于 5 个时,绝不要使用。
- R2.2:导航栏项目始终显示标签。不允许使用仅含图标的导航栏。
- R2.3:选中状态使用实心图标,未选中状态使用线框图标。
- R2.4:活动指示器使用
secondaryContainer颜色。不要覆盖此设置。
2.2 导航栏架
适用于中等和扩展屏幕(平板、折叠屏设备、桌面设备)。
// Compose: Navigation Rail for larger screens
NavigationRail(
header = {
FloatingActionButton(
onClick = { /* primary action */ },
containerColor = MaterialTheme.colorScheme.tertiaryContainer
) {
Icon(Icons.Default.Add, contentDescription = "Create")
}
}
) {
items.forEachIndexed { index, item ->
NavigationRailItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
规则:
- R2.5:窗口尺寸为中等(600-839dp)和扩展(840dp+)时使用 Navigation Rail。在紧凑窗口中搭配 Navigation Bar 使用。
- R2.6:可以选择在导航栏架的标题区域加入 FAB,用于主要操作。
- R2.7:导航栏架上的标签可选,但建议添加以便于理解。
2.3 导航抽屉
适用于具有 5 个以上目的地或复杂导航层级的场景,通常用于扩展屏幕。
// Compose: Permanent Navigation Drawer for large screens
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet {
Text("App Name", modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleMedium)
HorizontalDivider()
items.forEach { item ->
NavigationDrawerItem(
label = { Text(item.label) },
selected = item == selectedItem,
onClick = { selectedItem = item },
icon = { Icon(item.icon, contentDescription = null) }
)
}
}
}
) {
Scaffold { /* page content */ }
}
规则:
- R2.8:紧凑屏幕使用模态抽屉,扩展屏幕使用永久抽屉。
- R2.9:使用分隔线和分区标题将抽屉项目分组。
2.4 预测性返回手势
Android 13+ 支持带动画预览的预测性返回。
// Compose: Predictive back with BackHandler (androidx.activity.compose)
BackHandler(enabled = true) {
// Called when back is confirmed; navigate back in your nav controller
navController.popBackStack()
}
// Compose: Predictive back progress animation using predictiveBackHandler modifier
// (androidx.activity:activity-compose 1.8+)
Modifier.predictiveBackHandler(enabled = true) { progress ->
// progress is a Flow<BackEventCompat> with x, y, swipeEdge, progress (0.0–1.0)
progress.collect { backEvent ->
animationState = backEvent.progress
}
}
<application android:enableOnBackInvokedCallback="true">
规则:
- R2.10:在清单中启用预测性返回。在 Compose 应用中,使用
BackHandler(来自androidx.activity.compose)拦截返回事件。在基于 View 的应用中,实现OnBackInvokedCallback(API 33+)或OnBackPressedCallback(AndroidX),而不是重写onBackPressed()。 - R2.11:系统返回手势会返回导航栈中的上一个页面。“向上”按钮(工具栏箭头)会返回应用层级中的上一级。这两者可能不同。
- R2.12:除非存在尚未保存的用户输入,否则绝不要拦截系统返回来显示“确定要退出吗?”对话框。
- R2.13:不要禁用系统提供的返回预览动画。如果实现自定义进入/退出过渡动画,请使用
BackEventCompat.progress(0.0–1.0)进行插值,并遵循BackEventCompat.swipeEdge(EDGE_LEFT/EDGE_RIGHT),使退出屏幕缩小并向触发手势的一侧移动,以匹配系统动画。 - R2.14:优先帮助用户识别,而不是回忆。为目的地添加标签、显示选中状态并保留返回栈上下文,避免用户每次导航后都要重新判断自己所在的位置。
// Compose: drive a custom animation from predictive back progress
Modifier.predictiveBackHandler(enabled = true) { progress ->
progress.collect { backEvent ->
// backEvent.progress: 0.0 (gesture start) → 1.0 (committed)
// backEvent.swipeEdge: BackEventCompat.EDGE_LEFT or EDGE_RIGHT
exitScale = 1f - (backEvent.progress * 0.1f)
exitOffsetX = if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) -backEvent.progress * 32.dp.toPx() else backEvent.progress * 32.dp.toPx()
}
}
2.5 导航组件选择
| 屏幕尺寸 | 3-5 个目的地 | 5 个以上目的地 |
|---|---|---|
| 紧凑(< 600dp) | 导航栏 | 模态抽屉 + 导航栏 |
| 中等(600-839dp) | 导航轨道 | 模态抽屉 + 导航轨道 |
| 扩展(840dp+) | 导航轨道 | 固定抽屉 |
3. 布局与自适应 [HIGH]
3.1 窗口尺寸类别
使用窗口尺寸类别实现自适应布局,而非使用原始像素断点。
// Compose: Window size classes
val windowSizeClass = calculateWindowSizeClass(this)
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> CompactLayout()
WindowWidthSizeClass.Medium -> MediumLayout()
WindowWidthSizeClass.Expanded -> ExpandedLayout()
}
| 类别 | 宽度 | 典型设备 | 列数 |
|---|---|---|---|
| 紧凑 | < 600dp | 手机竖屏 | 4 |
| 中等 | 600-839dp | 平板竖屏、折叠设备 | 8 |
| 扩展 | 840dp+ | 平板横屏、桌面设备 | 12 |
规则:
- R3.1:始终使用
material3-window-size-class中的WindowSizeClass来决定响应式布局。 - R3.2:绝不要使用固定像素断点。设备类别是动态变化的。
- R3.3:支持全部三种宽度尺寸类别,至少支持紧凑和扩展类别。
3.2 Material 网格
采用 Material 网格规范中的标准边距和间距。
| 尺寸类别 | 边距 | 间距 | 列数 |
|---|---|---|---|
| 紧凑 | 16dp | 8dp | 4 |
| 中等 | 24dp | 16dp | 8 |
| 扩展 | 24dp | 24dp | 12 |
规则:
- R3.4:扩展屏幕上的内容不应占满全宽。请使用约 840dp 的最大内容宽度,或采用列表-详情布局。
- R3.5:应用与网格规范一致的水平边距。
3.3 边到边显示
Android 15+ 强制使用边到边布局。所有应用都应绘制到系统栏后方。
// Compose: Edge-to-edge setup
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
Scaffold(
modifier = Modifier.fillMaxSize(),
// Scaffold handles insets for top/bottom bars automatically
) { innerPadding ->
Content(modifier = Modifier.padding(innerPadding))
}
}
}
}
规则:
- R3.6:在
setContent之前调用enableEdgeToEdge()。在状态栏和导航栏后方绘制内容。 - R3.7:使用
WindowInsets为内容添加内边距,使其避开系统栏。Scaffold会自动处理顶部栏和底部栏内容。 - R3.8:可滚动内容应在透明系统栏后方滚动,并在列表顶部和底部添加适当的内嵌边距。
3.4 折叠设备支持
// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
.windowLayoutInfo(context)
.collectAsState(initial = WindowLayoutInfo(emptyList()))
规则:
- R3.9:检测铰链/折叠位置,避免将关键内容放置在折痕上。
- R3.10:使用 Material3 自适应库中的
ListDetailPaneScaffold或SupportingPaneScaffold,实现适配折叠设备的布局。
4. 字体排版 [HIGH]
4.1 Material 字体比例
| 角色 | 默认字号 | 默认字重 | 用途 |
|---|---|---|---|
| displayLarge | 57sp | 400 | 主视觉文本、引导流程 |
| displayMedium | 45sp | 400 | 大型功能文本 |
| displaySmall | 36sp | 400 | 突出的展示文本 |
| headlineLarge | 32sp | 400 | 屏幕标题 |
| headlineMedium | 28sp | 400 | 分区标题 |
| headlineSmall | 24sp | 400 | 卡片标题 |
| titleLarge | 22sp | 400 | 顶部应用栏标题 |
| titleMedium | 16sp | 500 | 标签页、导航 |
| titleSmall | 14sp | 500 | 副标题 |
| bodyLarge | 16sp | 400 | 主要正文 |
| bodyMedium | 14sp | 400 | 次要正文 |
| bodySmall | 12sp | 400 | 说明文字 |
| labelLarge | 14sp | 500 | 按钮、醒目的标签 |
| labelMedium | 12sp | 500 | 筛选标签、较小的标签 |
| labelSmall | 11sp | 500 | 时间戳、注释 |
// Compose: Custom typography
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily(Font(R.font.brand_regular)),
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily(Font(R.font.brand_regular)),
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
// ... define all 15 roles
)
规则:
- R4.1:文本字号始终使用
sp单位,以支持用户的字体缩放偏好。 - R4.2:正文文本不得小于 12sp。标签最小可使用 11sp。
- R4.3:从
MaterialTheme.typography引用排版角色,不要硬编码字号。 - R4.4:支持动态字体缩放。请在 200% 字体缩放比例下测试,确保没有文本被裁切或重叠。
- R4.5:为保证可读性,行高应约为字号的 1.2–1.5 倍。
5. 组件 [HIGH]
5.1 悬浮操作按钮(FAB)
FAB 表示屏幕上最重要的单个操作。
// Compose: FAB variants
// Standard FAB
FloatingActionButton(onClick = { /* action */ }) {
Icon(Icons.Default.Add, contentDescription = "Create new item")
}
// Extended FAB (with label - preferred for clarity)
ExtendedFloatingActionButton(
onClick = { /* action */ },
icon = { Icon(Icons.Default.Edit, contentDescription = null) },
text = { Text("Compose") }
)
// Large FAB
LargeFloatingActionButton(onClick = { /* action */ }) {
Icon(Icons.Default.Add, contentDescription = "Create", modifier = Modifier.size(36.dp))
}
规则:
- R5.1:每个屏幕最多使用一个 FAB,用于表示主要操作。
- R5.2:将 FAB 放在屏幕底部末端。在带有 Navigation Bar 的屏幕上,FAB 悬浮于其上方。
- R5.3:FAB 默认应使用
primaryContainer颜色。次要屏幕使用tertiaryContainer。 - R5.4:为提升清晰度,优先使用带标签的
ExtendedFloatingActionButton。必要时,可在滚动时收起为仅图标。
5.2 顶部应用栏
// Compose: Top app bar variants
// Small (default)
TopAppBar(
title = { Text("Page Title") },
navigationIcon = {
IconButton(onClick = { /* navigate up */ }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* search */ }) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
}
)
// Medium — expands title area
MediumTopAppBar(
title = { Text("Section Title") },
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
)
// Large — for prominent titles
LargeTopAppBar(
title = { Text("Screen Title") },
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
)
规则:
- R5.5:大多数屏幕使用
TopAppBar(小型)。对于醒目的分区标题或屏幕标题,使用MediumTopAppBar或LargeTopAppBar。 - R5.6:将滚动行为连接到应用栏,使其随内容滚动而收起或展开。
- R5.7:操作图标限制为 2–3 个。将其他操作收纳到更多菜单中。
5.3 底部工作表
// Compose: Modal bottom sheet
ModalBottomSheet(
onDismissRequest = { showSheet = false },
sheetState = rememberModalBottomSheetState()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Sheet Title", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
// Sheet content
}
}
规则:
- R5.8:非关键补充内容使用模态底部工作表。持久显示的内容使用标准底部工作表。
- R5.9:底部工作表必须显示拖动手柄,以便用户发现其可拖动。
- R5.10:如果工作表内容可能超出可视区域,则必须支持滚动。
5.4 对话框
// Compose: Alert dialog
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Discard draft?") },
text = { Text("Your unsaved changes will be lost.") },
confirmButton = {
TextButton(onClick = { /* confirm */ }) { Text("Discard") }
},
dismissButton = {
TextButton(onClick = { showDialog = false }) { Text("Cancel") }
}
)
规则:
- R5.11:对话框会打断用户。仅在需要立即关注的关键决策场景中使用。
- R5.12:确认按钮使用文本按钮,而非填充按钮。关闭按钮始终位于左侧。
- R5.13:对话框标题应简洁明了,可采用问题或陈述句。正文提供背景信息。
5.5 Snackbar
// 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:使用 snackbar 提供简短的非关键反馈。它会自动关闭,不应包含关键信息。
- R5.15:Snackbar 显示在屏幕底部,位于 Navigation Bar 上方、FAB 下方。
- R5.16:操作可撤销时,添加操作项(例如“撤销”)。操作项最多一个。
5.6 筛选标签
// Filter Chip
FilterChip(
selected = isSelected,
onClick = { isSelected = !isSelected },
label = { Text("Filter") },
leadingIcon = if (isSelected) {
{ Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(18.dp)) }
} else null
)
// Assist Chip
AssistChip(
onClick = { /* action */ },
label = { Text("Add to calendar") },
leadingIcon = { Icon(Icons.Default.CalendarToday, contentDescription = null) }
)
规则:
- R5.17:使用
FilterChip切换筛选条件,使用AssistChip提供智能建议,使用InputChip显示用户输入的内容(标签),使用SuggestionChip显示动态生成的建议。 - R5.18:筛选标签应排列在可水平滚动的行或流式布局中,不要垂直堆叠。
- R5.19:立即显示等待状态。如果操作无法立即完成,应通过内联状态变化、进度提示或其他可见反馈予以响应,而不是让界面保持静止。
5.7 组件选择指南
| 需求 | 组件 |
|---|---|
| 主要屏幕操作 | FAB |
| 简短反馈 | Snackbar |
| 关键决策 | Dialog |
| 补充内容 | Bottom Sheet |
| 切换筛选条件 | Filter Chip |
| 用户输入的标签 | Input Chip |
| 智能建议 | Assist Chip |
| 内容分组 | Card |
| 垂直项目列表 | 带有 ListItem 的 LazyColumn |
| 分段选项(2-5 个) | SegmentedButton |
| 二元切换 | Switch |
| 从列表中选择 | 单选按钮或展开式下拉菜单 |
6. 无障碍功能 [CRITICAL]
6.1 TalkBack 和内容描述
// Compose: Accessible components
Icon(
Icons.Default.Favorite,
contentDescription = "Add to favorites" // Descriptive, not "heart icon"
)
// Decorative elements
Icon(
Icons.Default.Star,
contentDescription = null // null for purely decorative
)
// Merge semantics for compound elements
Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
Icon(Icons.Default.Event, contentDescription = null)
Text("March 15, 2026")
}
// Custom actions
Box(modifier = Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction("Archive") { /* archive */ true },
CustomAccessibilityAction("Delete") { /* delete */ true }
)
})
规则:
- R6.1:每个可交互元素都必须设置
contentDescription(如果纯粹用于装饰,则设置为null)。 - R6.2:内容描述必须说明操作或含义,而非视觉外观。应说“添加到收藏”,而不是“心形图标”。
- R6.3:使用
mergeDescendants = true将相关元素组合成单个 TalkBack 焦点单元(例如包含图标、文本和副标题的列表项)。 - R6.4:为滑动关闭或长按操作提供
customActions,以便 TalkBack 用户使用这些操作。
6.2 触控目标
// Compose: Ensure minimum touch target
IconButton(onClick = { /* action */ }) {
// IconButton already provides 48dp minimum touch target
Icon(Icons.Default.Close, contentDescription = "Close")
}
// Manual minimum touch target
Box(
modifier = Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.clickable { /* action */ },
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Info, contentDescription = "Info", modifier = Modifier.size(24.dp))
}
规则:
- R6.5:所有可交互元素的最小触控目标必须为 48x48dp。Material 3 组件默认会处理此要求。
- R6.6:不要为了节省空间而缩小触控目标。如果视觉元素较小,请使用内边距扩大可触控区域。
6.3 颜色对比度和视觉效果
规则:
- R6.7:普通文本与背景的对比度至少须为 4.5:1,大号文本(18sp+ 或 14sp+ 粗体)至少须为 3:1。
- R6.8:切勿仅用颜色传达信息。应搭配图标、文本或图案。
- R6.9:支持粗体文本和高对比度无障碍设置。使用
Configuration.fontWeightAdjustment(API 31+)检测用户的粗体文本偏好,并相应调整自定义字体粗细。使用AccessibilityManager.isHighTextContrastEnabled()检测高对比度模式,并替换为对比度更高的颜色值。Material 3 组件会自动处理这两项;自定义文本渲染和颜色用法必须显式启用。
// Detect bold text preference (API 31+)
val fontWeightAdjustment = resources.configuration.fontWeightAdjustment
val isBoldText = fontWeightAdjustment >= 700 // equivalent to FontWeight.Bold.weight
// Detect high contrast mode
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
val isHighContrast = am.isHighTextContrastEnabled
// Compose: use MaterialTheme.typography which respects fontWeightAdjustment automatically
Text(
text = "Label",
style = MaterialTheme.typography.bodyLarge // Adapts to fontWeightAdjustment
)
// For custom colors: provide high-contrast alternative
val labelColor = if (isHighContrast) {
MaterialTheme.colorScheme.onSurface // Strong contrast
} else {
MaterialTheme.colorScheme.onSurfaceVariant // Normal contrast
}
6.4 焦点和遍历顺序
// Compose: Custom focus order
Column {
var focusRequester = remember { FocusRequester() }
TextField(
modifier = Modifier.focusRequester(focusRequester),
value = text,
onValueChange = { text = it }
)
LaunchedEffect(Unit) {
focusRequester.requestFocus() // Auto-focus on screen load
}
}
规则:
- R6.10:焦点顺序必须遵循符合逻辑的阅读顺序(从上到下、从起始端到末端)。除非默认顺序有误,否则避免自定义
focusOrder。 - R6.11:导航或关闭对话框后,将焦点移至最符合逻辑的目标元素。
- R6.12:所有屏幕都必须能够通过 TalkBack、Switch Access 和外接键盘完整操作。
6.5 自定义 Canvas 视图
在 Canvas 上绘制内容的自定义 View 子类(图表、自定义选择器、绘图区域)默认对 TalkBack 不可见,因为它们没有子视图。使用 androidx.customview.widget 中的 ExploreByTouchHelper 来定义虚拟无障碍树。
- R6.13:通过 Canvas 绘制的自定义视图必须使用
ExploreByTouchHelper,向 TalkBack 提供虚拟无障碍树。重写getVirtualViewAt(),将触摸坐标映射到虚拟视图 ID;并重写onPopulateNodeForVirtualView(),为每个虚拟节点提供文本、边界和操作。
import androidx.customview.widget.ExploreByTouchHelper
class PieChartView(context: Context) : View(context) {
private val helper = object : ExploreByTouchHelper(this) {
override fun getVirtualViewAt(x: Float, y: Float): Int {
// Return virtual view ID for the slice at (x, y), or INVALID_ID
return sliceIndexAt(x, y)
}
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
slices.indices.forEach { virtualViewIds.add(it) }
}
override fun onPopulateNodeForVirtualView(
virtualViewId: Int,
node: AccessibilityNodeInfoCompat
) {
val slice = slices[virtualViewId]
node.text = "${slice.label}: ${slice.percentage}%"
node.setBoundsInParent(slice.bounds)
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
}
override fun onPerformActionForVirtualView(
virtualViewId: Int, action: Int, arguments: Bundle?
): Boolean {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
onSliceSelected(virtualViewId)
return true
}
return false
}
}
init {
ViewCompat.setAccessibilityDelegate(this, helper)
}
override fun dispatchHoverEvent(event: MotionEvent) =
helper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event)
}
7. 手势与输入 [MEDIUM]
7.1 系统手势
规则:
- R7.1:切勿将可交互元素放在系统手势内边距区域(底部 20dp、左右边缘 24dp)内,因为它们会与系统导航手势冲突。
- R7.2:使用
WindowInsets.systemGestures检测并避开手势冲突区域。
7.2 常见手势模式
// Compose: Pull to refresh
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = { viewModel.refresh() }
) {
LazyColumn { /* content */ }
}
// Compose: Swipe to dismiss
SwipeToDismissBox(
state = rememberSwipeToDismissBoxState(),
backgroundContent = {
Box(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.error),
contentAlignment = Alignment.CenterEnd
) {
Icon(Icons.Default.Delete, contentDescription = "Delete",
tint = MaterialTheme.colorScheme.onError)
}
}
) {
ListItem(headlineContent = { Text("Swipeable item") })
}
规则:
- R7.3:所有滑动关闭操作都必须支持撤销(显示带有撤销操作的 snackbar),或要求确认。
- R7.4:为所有基于手势的操作提供非手势替代方式(以确保无障碍使用)。
- R7.5:在所有可点击元素上应用 Material 涟漪效果。Compose 的
clickable修饰符默认包含涟漪效果。
7.3 长按
规则:
- R7.6:长按用于打开上下文菜单和进入多选模式。不得将长按作为访问某项功能的唯一方式。
- R7.7:长按时通过
HapticFeedbackType.LongPress提供触觉反馈。
8. 通知 [MEDIUM]
8.1 通知渠道
// Create notification channel (required for Android 8+)
val channel = NotificationChannel(
"messages",
"Messages",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "New message notifications"
enableLights(true)
lightColor = Color.BLUE
}
notificationManager.createNotificationChannel(channel)
| 重要程度 | 行为 | 用途 |
|---|---|---|
| IMPORTANCE_HIGH | 声音 + 抬头通知 | 消息、通话 |
| IMPORTANCE_DEFAULT | 声音 | 社交动态、电子邮件 |
| IMPORTANCE_LOW | 无声音 | 推荐 |
| IMPORTANCE_MIN | 静音,不显示在状态栏 | 天气、进行中的操作 |
规则:
- R8.1:为每种不同类型的通知创建独立的通知渠道。用户可以分别配置各个渠道。
- R8.2:谨慎选择重要程度。过度使用
IMPORTANCE_HIGH会导致用户彻底关闭通知。 - R8.3:所有通知都必须包含点击操作(PendingIntent),以导航到相关内容。
- R8.4:为通知图标添加
contentDescription,以支持无障碍功能。
8.2 通知设计
规则:
- R8.5:对话使用
MessagingStyle,并包含发件人姓名和头像。 - R8.6:为消息通知添加直接回复操作。
- R8.7:为消息通知提供“标记为已读”操作。
- R8.8:对富内容使用可展开通知(
BigTextStyle、BigPictureStyle、InboxStyle)。 - R8.9:前台服务通知必须准确描述正在进行的操作,并在适当情况下提供停止操作。
9. 权限与隐私 [HIGH]
9.1 运行时权限
// Compose: Permission request
val permissionState = rememberPermissionState(Manifest.permission.CAMERA)
if (permissionState.status.isGranted) {
CameraPreview()
} else {
Column {
Text("Camera access is needed to scan QR codes.")
Button(onClick = { permissionState.launchPermissionRequest() }) {
Text("Grant Camera Access")
}
}
}
规则:
- R9.1:在合适的场景、需要权限时再请求,而不是在应用启动时请求。
- R9.2:请求权限前,务必说明请求原因(理由说明界面)。
- R9.3:妥善处理权限被拒绝的情况。应提供功能受限的使用方式,而不是阻止用户使用。
- R9.4:切勿请求未实际使用的权限。Google Play 会拒绝权限申请不必要的应用。
9.2 注重隐私保护的 API
// Photo picker: no permission needed
val pickMedia = rememberLauncherForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let { /* handle selected media */ }
}
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
规则:
- R9.5:使用 Photo Picker(Android 13+),而不是请求
READ_MEDIA_IMAGES。无需权限。 - R9.6:除非功能必须使用精确位置,否则使用
ACCESS_COARSE_LOCATION(大致位置)。 - R9.7:在非录制场景中,优先为相机和麦克风使用一次性权限。
- R9.8:相机或麦克风正在使用时,显示隐私指示器。
10. 系统集成 [MEDIUM]
10.1 小部件
// Compose Glance API widget
class TaskWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(16.dp)
) {
Text(
text = "Tasks",
style = TextStyle(fontWeight = FontWeight.Bold,
color = GlanceTheme.colors.onSurface)
)
// Widget content
}
}
}
}
}
规则:
- R10.1:新建小部件时使用 Glance API。通过
GlanceTheme支持动态颜色。 - R10.2:小部件必须有默认配置,并能在放置后立即使用。
- R10.3:条件允许时,提供多种小部件尺寸(小、中、大)。
- R10.4:使用与系统小部件形状匹配的圆角(
system_app_widget_background_radius)。
10.2 应用快捷方式
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="compose"
android:enabled="true"
android:shortcutShortLabel="@string/compose_short"
android:shortcutLongLabel="@string/compose_long"
android:icon="@drawable/ic_shortcut_compose">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.example.app"
android:targetClass="com.example.app.ComposeActivity" />
</shortcut>
</shortcuts>
规则:
- R10.5:为常用操作提供 2-4 个静态快捷方式,并为最近内容提供动态快捷方式。
- R10.6:快捷方式图标应采用简洁、易识别的剪影,并置于圆形背景上。
- R10.7:在应用图标上长按,以及在“设置”>“应用”的快捷方式列表中测试快捷方式。
10.3 深层链接与分享
规则:
- R10.8:为所有公开内容 URL 支持 Android App Links(已验证的深层链接)。
- R10.9:使用
ShareCompat或Intent.createChooser实现分享面板。提供包含标题、描述和缩略图的富预览。 - R10.10:通过适当的内容类型过滤来处理传入的分享 Intent。
设计评估清单
使用此清单评估 Android UI 实现:
主题与颜色
- 已启用动态颜色,并提供静态回退方案
- 所有颜色均引用 Material 主题角色(不使用硬编码的十六进制值)
- 同时支持浅色和深色主题
- 前景色与对应的背景颜色角色匹配
- 使用 Material Theme Builder 根据种子生成自定义颜色
导航
- 根据屏幕尺寸和目的地数量选择正确的导航组件
- 始终显示导航栏标签
- 启用并处理预测性返回手势
- 正确处理向上导航与返回行为
布局
- 支持全部三种窗口尺寸类别
- 实现边到边布局并正确处理内边距
- 大屏幕上的内容不应占满整个宽度
- 尊重折叠设备的铰链区域
排版
- 所有文本均使用 sp 单位
- 所有文本均引用 MaterialTheme.typography 中的样式
- 在 200% 字体缩放下完成测试,确保没有裁切
- 正文至少为 12sp,标签至少为 11sp
组件
- 每个屏幕最多使用一个 FAB
- 顶部应用栏与滚动行为关联
- Snackbar 仅用于非关键反馈
- 对话框仅用于需要关键中断的情形
无障碍
- 所有可交互元素都设置了 contentDescription
- 所有触控目标均 >= 48dp
- 文本的颜色对比度均 >= 4.5:1
- 不单独依靠颜色传达信息
- 完整测试 TalkBack 遍历
- Switch Access 和键盘导航均可正常使用
手势
- 系统手势区域内没有可交互元素
- 所有手势操作都有非手势替代方式
- 滑动关闭操作可以撤销
通知
- 为每种通知类型设置单独的渠道
- 设置适当的重要性级别
- 点按操作可导航到相关内容
权限
- 在适当的使用情境中请求权限,而不是在启动时请求
- 请求权限前显示原因说明
- 被拒绝时优雅降级
- 使用 Photo Picker 代替媒体权限
系统集成
- 微件使用 Glance API 和动态颜色
- 为常见操作提供应用快捷方式
- 处理指向公开内容的深层链接
反模式
| 反模式 | 错误原因 | 正确做法 |
|---|---|---|
| 硬编码颜色十六进制值 | 破坏动态颜色和深色主题 | 使用 MaterialTheme.colorScheme 中的样式 |
使用 dp 设置文字大小 | 忽略用户的字体缩放设置 | 使用 sp 单位 |
| 自定义底部导航栏 | 与平台不一致 | 使用 Material NavigationBar |
| 导航栏不显示标签 | 违反 Material 指南 | 始终显示标签 |
| 使用对话框展示非关键信息 | 不必要地打断用户 | 使用 Snackbar 或 Bottom Sheet |
| 用 FAB 执行次要操作 | 削弱主要操作的突出程度 | 仅为主要操作使用一个 FAB |
重写 onBackPressed() | 已弃用;会破坏预测性返回 | 使用 BackHandler(Compose)或 OnBackInvokedCallback(基于 View)以支持预测性返回 |
| 触控目标 < 48dp | 违反无障碍要求 | 确保最小尺寸为 48x48dp |
| 在启动时请求权限 | 用户会因缺乏情境而拒绝 | 在适当的使用情境中请求权限并说明原因 |
| 深色主题使用纯黑色(#000000) | 容易造成眼疲劳;不符合 Material 3 | 使用 Material 表面颜色样式 |
| 导航栏仅显示图标 | 用户无法识别目的地 | 始终包含文字标签 |
| 平板上使用全宽内容 | 浪费空间;可读性差 | 设置最大宽度或使用列表-详情布局 |
用于照片的 READ_EXTERNAL_STORAGE | Android 13 起不再需要 | 使用 Photo Picker API |
| 权限被拒后阻断界面 | 对用户造成惩罚 | 优雅降级 |
| 手动选择调色板 | 色调关系不一致 | 使用 Material Theme Builder |