mcpField
A
android-design-guidelines

android-design-guidelines

github.com/ehmo设计与 UI6311.7k 次安装32 周前更新
代码仓库

关于 android-design-guidelines

面向 Android 界面设计与实现的 Material Design 3 指南 Skill,涵盖动态配色、导航、响应式布局、排版及折叠设备适配。

主要功能

  1. 01动态颜色与主题角色规范
  2. 02按屏幕尺寸选择导航组件
  3. 03窗口尺寸类响应式布局建议
  4. 04边到边显示与系统栏内边距指导
  5. 05折叠设备布局建议

适用场景

  1. 01为 Android 应用配置动态主题
  2. 02规划手机、平板和大屏导航
  3. 03设计适配不同窗口尺寸的布局
  4. 04检查折叠设备铰链区域的内容放置

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/android-design-guidelines确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

AGENTS.md
metadata.json
rules/
_sections.md

详细介绍

Android 平台设计指南 — Material Design 3

1. Material You 与主题 [CRITICAL]

1.1 动态颜色

启用根据用户壁纸生成的动态颜色。动态颜色是 Android 12+ 的默认选项,并应作为首选主题策略。

// Compose: Dynamic color theme
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> darkColorScheme()
        else -> lightColorScheme()
    }
    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        content = content
    )
}

<style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.DayNight</item>
</style>

规则:

  • R1.1:始终为 Android 12 以下的设备提供静态颜色方案作为回退。
  • R1.2:绝不要在组件中硬编码颜色的十六进制值。始终引用主题中的颜色角色。
  • R1.3:至少使用 3 种不同的壁纸进行测试,以验证动态颜色的协调性。
1.2 颜色角色

Material 3 定义了一套结构化的颜色角色。请按语义而非审美用途使用它们。

角色用途对应角色
primary关键操作、活动状态、FABonPrimary
primaryContainer不那么醒目的主要元素onPrimaryContainer
secondary辅助 UI、筛选芯片onSecondary
secondaryContainer导航栏活动指示器onSecondaryContainer
tertiary点缀、对比、互补onTertiary
tertiaryContainer输入框、不那么醒目的点缀onTertiaryContainer
surface背景、卡片、底部面板onSurface
surfaceVariant装饰元素、分隔线onSurfaceVariant
error错误状态、破坏性操作onError
errorContainer错误背景onErrorContainer
outline边框、分隔线—
outlineVariant细微边框—
inverseSurfaceSnackbar 背景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 网格规范中的标准边距和间距。

尺寸类别边距间距列数
紧凑16dp8dp4
中等24dp16dp8
扩展24dp24dp12

规则:

  • R3.4:扩展屏幕上的内容不应占满全宽。请使用约 840dp 的最大内容宽度,或采用列表-详情布局。
  • R3.5:应用与网格规范一致的水平边距。
3.3 边到边显示

Android 15+ 强制使用边到边布局。所有应用都应绘制到系统栏后方。

// Compose: Edge-to-edge setup
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                // Scaffold handles insets for top/bottom bars automatically
            ) { innerPadding ->
                Content(modifier = Modifier.padding(innerPadding))
            }
        }
    }
}

规则:

  • R3.6:在 setContent 之前调用 enableEdgeToEdge()。在状态栏和导航栏后方绘制内容。
  • R3.7:使用 WindowInsets 为内容添加内边距,使其避开系统栏。Scaffold 会自动处理顶部栏和底部栏内容。
  • R3.8:可滚动内容应在透明系统栏后方滚动,并在列表顶部和底部添加适当的内嵌边距。
3.4 折叠设备支持
// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
    .windowLayoutInfo(context)
    .collectAsState(initial = WindowLayoutInfo(emptyList()))

规则:

  • R3.9:检测铰链/折叠位置,避免将关键内容放置在折痕上。
  • R3.10:使用 Material3 自适应库中的 ListDetailPaneScaffold 或 SupportingPaneScaffold,实现适配折叠设备的布局。

4. 字体排版 [HIGH]

4.1 Material 字体比例
角色默认字号默认字重用途
displayLarge57sp400主视觉文本、引导流程
displayMedium45sp400大型功能文本
displaySmall36sp400突出的展示文本
headlineLarge32sp400屏幕标题
headlineMedium28sp400分区标题
headlineSmall24sp400卡片标题
titleLarge22sp400顶部应用栏标题
titleMedium16sp500标签页、导航
titleSmall14sp500副标题
bodyLarge16sp400主要正文
bodyMedium14sp400次要正文
bodySmall12sp400说明文字
labelLarge14sp500按钮、醒目的标签
labelMedium12sp500筛选标签、较小的标签
labelSmall11sp500时间戳、注释
// Compose: Custom typography
val AppTypography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_regular)),
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_regular)),
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    )
    // ... define all 15 roles
)

规则:

  • R4.1:文本字号始终使用 sp 单位,以支持用户的字体缩放偏好。
  • R4.2:正文文本不得小于 12sp。标签最小可使用 11sp。
  • R4.3:从 MaterialTheme.typography 引用排版角色,不要硬编码字号。
  • R4.4:支持动态字体缩放。请在 200% 字体缩放比例下测试,确保没有文本被裁切或重叠。
  • R4.5:为保证可读性,行高应约为字号的 1.2–1.5 倍。

5. 组件 [HIGH]

5.1 悬浮操作按钮(FAB)

FAB 表示屏幕上最重要的单个操作。

// Compose: FAB variants
// Standard FAB
FloatingActionButton(onClick = { /* action */ }) {
    Icon(Icons.Default.Add, contentDescription = "Create new item")
}

// Extended FAB (with label - preferred for clarity)
ExtendedFloatingActionButton(
    onClick = { /* action */ },
    icon = { Icon(Icons.Default.Edit, contentDescription = null) },
    text = { Text("Compose") }
)

// Large FAB
LargeFloatingActionButton(onClick = { /* action */ }) {
    Icon(Icons.Default.Add, contentDescription = "Create", modifier = Modifier.size(36.dp))
}

规则:

  • R5.1:每个屏幕最多使用一个 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_STORAGEAndroid 13 起不再需要使用 Photo Picker API
权限被拒后阻断界面对用户造成惩罚优雅降级
手动选择调色板色调关系不一致使用 Material Theme Builder

评论

支持 Markdown