UX工程指南 是做什么的?
一套实用的、经过生产验证的操作框架,用于使用Jetpack Compose、Material 3、离线优先弹性和以人为本的移动端人机工学设计来构建原生Android应用。
1. 核心移动端人机工学与拇指操作区域
移动设备超过70%的时间是单手操作的。设计界面时应围绕拇指可达范围、触觉反馈和最小的物理摩擦来展开。
1.1 人体工学可达性层级
+---------------------------+ <- 屏幕顶部(仅限显示)
| 状态栏与标题 | 最难触及。保留用于被动状态、
| 标题、配额仪表 | 搜索栏和实时仪表。
+---------------------------+
| | <- 中间可达区域
| 可滚动内容 | 适合列表项、详情和
| 收件箱卡片、片段 | 可浏览的内容卡片。
| |
+---------------------------+
| 主要操作目标 | <- 最佳拇指区域(底部40%)
| 发送按钮、标签页 | 最易点击。将导航、行动号召(CTA)
| 底部弹窗、筛选器 | 和交互控件保留于此。
+---------------------------+
1.2 不可妥协的触摸目标标准
- 最小交互目标: 每个可点击元素必须达到或超过 $48 \times 48\text{dp}$(
Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)),即使视觉图标只有 $24\text{dp}$。 - 边缘内边距与安全区域: 始终在根级Compose布局中使用
Scaffold或应用Modifier.windowInsetsPadding(WindowInsets.safeDrawing),以应对摄像头开孔、手势和导航栏。 - 表单控件置于底部: 将文本输入、提交按钮和确认对话框放在
ModalBottomSheet或底部固定的卡片中,使软件键盘(imePadding())不会遮挡当前活动字段。
2. 速度与感知性能
移动端用户会立刻感知到延迟。决不要阻塞导航或在已有先前获取的数据时显示空白加载状态。
2.1 零骨架屏规则(Stale-While-Revalidate)
- 问题: 每次用户点击收件箱标签页时都显示全页骨架加载器或圆形旋转指示器,会让人感觉缓慢、突兀且不精致。
- 解决方案: 实现Stale-While-Revalidate(SWR)缓存模式:
- 将获取的项目存储在本地加密存储中(
EncryptedSharedPreferences或SQLite/Room)。 - 进入屏幕时,立即渲染缓存的项目(0毫秒延迟)。
- 如果缓存超过5-10分钟,则在后台重新验证。
- 当刷新数据到达时,平滑地追加/更新项目,而不清除列表或显示空白占位。
- 仅在后台刷新期间显示微妙的指示器(例如,下拉刷新旋转器或顶部的小进度条)。
- 将获取的项目存储在本地加密存储中(
2.2 离线优先的发件箱模式
即使移动设备网络连接不稳定或暂时处于飞行模式,也绝不让用户操作失败。
flowchart TD
UserAction["用户分享文件 / 输入笔记"] --> LocalDraft["1. 保存到本地发件箱草稿"]
LocalDraft --> RenderOutbox["2. 立即在发件箱队列卡片中渲染"]
RenderOutbox --> CheckNet{"已连接网络?"}
CheckNet -- 是 --> DirectUpload["3. 直接上传"]
CheckNet -- 否 --> EnqueueWork["3. 加入WorkManager任务(NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. 等待Wi-Fi / 蜂窝网络"]
WaitConn --> AutoDispatch["5. 后台上传,带指数退避"]
DirectUpload --> Success["6. 更新UI状态并通知Toast"]
AutoDispatch --> Success
发件箱UI状态:
- 待处理: "正在等待网络…" 带暂停的云图标。
- 上传中: "正在发送 42%…" 带线性进度指示器。
- 失败 / 重试中: 清晰的状态指示器,带有明确的重试和放弃按钮。
3. 动效、转场与触觉反馈
现代Android设计(Android 14/15+)需要基于物理的动态动效和触觉感官反馈,才能显得原生且响应迅速。
3.1 边缘到边缘与系统栏处理
始终在MainActivity.kt中全局绘制到系统栏后面:
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHost(Modifier.padding(innerPadding))
}
}
}
3.2 预测性返回手势
在AndroidManifest.xml中启用预测性返回(android:enableOnBackInvokedCallback="true"),并在底部弹窗和详情抽屉中使用PredictiveBackHandler:
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// 按backEvent.progress(0f到1f)比例缩放或平移详情抽屉
detailScale = 1f - (backEvent.progress * 0.1f)
}
// 用户提交了返回手势
closeDetail()
} catch (e: CancellationException) {
// 用户取消了返回手势,弹回1f
detailScale = 1f
}
}
3.3 共享元素转场
当从收件箱列表行过渡到项目详情视图时,将屏幕包裹在SharedTransitionLayout中:
SharedTransitionLayout {
NavHost(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
在缩略图卡片、头像图标和标题上使用.sharedElement()或.sharedBounds()来创建无缝转场。
3.4 触觉反馈
当用户操作达到物理里程碑时,绝不能让动作保持静默:
- 阈值滑动(滑动删除): 当滑动超过操作阈值时,触发
HapticFeedbackConstantsCompat.GESTURE_START。 - 复制到剪贴板 / 发送完成: 触发
HapticFeedbackConstantsCompat.CONFIRM。 - 验证失败 / 网络错误: 触发
HapticFeedbackConstantsCompat.REJECT。
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 弹簧物理动画优于线性缓动
避免僵硬的线性动画。在手势、弹窗和滑动移除中始终使用弹簧物理动画:
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. 智能一键操作与系统集成
移动端界面应通过识别负载类型并提供最相关的原生系统操作,来最小化认知负担。
4.1 上下文感知操作矩阵
| 负载类型 | 主要操作 | 次要操作 | 系统集成 |
|---|---|---|---|
| URL / 网页链接 | 在浏览器中打开(Intent.ACTION_VIEW) | 复制链接 | Chrome Custom Tabs 或系统默认浏览器 |
| 文本 / 片段 | 复制文本 | 分享(Intent.ACTION_SEND) | Android ClipboardManager 带即时Toast / Snackbar |
| 文件 / 媒体 | 下载到文件夹 | 打开文件 | 存储访问框架(SAF)+ FileProvider |
4.2 直接文件下载与自定义默认目录
每次用户下载文件时都提示系统文件选择器会造成不必要的摩擦。
- 允许用户在设置/中心使用
ActivityResultContracts.OpenDocumentTree()一次性地选择默认下载文件夹。 - 永久保留该权限:
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - 当点击下载时:
- 如果已设置默认目录:直接使用
DocumentFile.createFile()传输字节,显示实时字节进度,并触发原生完成Toast("已下载 document.pdf 到 Vault")。 - 如果未设置默认目录:回退到
ActivityResultContracts.CreateDocument()。
- 如果已设置默认目录:直接使用
4.3 使用FileProvider安全打开文件
从不使用不安全的原始file:// URI。始终通过FileProvider将文件查看委托给原生Android应用:
<!-- res/xml/file_paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cached_files" path="." />
</paths>
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "打开方式..."))
5. 无摩擦的移动端认证
在移动触摸屏上输入服务器URL和长密码容易出现拼写错误。
5.1 登录优先级层级
- 优先级1:二维码摄像头扫描(1秒配对)
- 在桌面Web应用上显示一个临时的、已签名的配对二维码。
- 移动端用户打开应用,点击扫描二维码,将摄像头对准屏幕。
- 应用在一次扫描中解析源、令牌和设备名称——立即完成认证。
- 优先级2:用户名和密码,带可折叠服务器URL
- 99%的登录使用默认配置的主机。
- 默认情况下仅显示用户名和密码字段。
- 将服务器URL隐藏在一个微妙的可折叠切换后:
"服务器配置(高级)"。
5.2 14天滑动会话持久化
- 将访问令牌(
va1_)和刷新令牌(vr1_)存储在Android的EncryptedSharedPreferences中。 - 在活跃使用期间自动滑动/轮换刷新令牌。
- 只要用户在14天内打开应用,他们就保持登录状态,无需重新认证。
6. 自适应布局(可折叠设备和平板)
使用WindowSizeClass构建能跨窗口尺寸类平滑伸缩的屏幕:
- 紧凑型(
<600dp): 单列,底部导航栏,模态底部弹窗。 - 中型(
600dp - 840dp): 导航导轨,两列网格。 - 展开型(
>840dp): 并排的持久主-从双窗格布局(左侧列表,右侧检查器)。
7. Material 3颜色对比度与状态可达性(WCAG 2.2 AA / AAA)
移动设备由于环境光线变化(直射阳光、室外眩光、OLED省电变暗)特别容易出现对比度失效。力争达到WCAG 2.2 AAA文本对比度($\ge 7:1$),并严格遵守WCAG 1.4.11(非文本边界对比度 $\ge 3:1$)和WCAG 1.4.1(颜色使用)。
7.1 Material 3暗色模式FilterChip陷阱
- 缺陷: 默认情况下,Material 3的
FilterChip在暗色模式下使用secondaryContainer(#37474F)作为选中状态的背景,而暗色surface为(#14181E)。这产生的对比度低至1.83:1。在低亮度或强光下,用户无法分辨哪个Chip是激活的。 - 修复:
- 使用
selectedBorderColor = MaterialTheme.colorScheme.primary强制定义高对比度边界(在暗色模式下产生9.88:1的对比度)。 - 设置
selectedBorderWidth = 1.5.dp。 - 遵循WCAG 1.4.1(状态不能仅依赖颜色),提供一个前导勾选图标:
FilterChip( selected = isSelected, onClick = { onSelect() }, label = { Text(label) }, leadingIcon = if (isSelected) { { Icon(Icons.Filled.Done, contentDescription = null, Modifier.size(16.dp)) } } else null, border = FilterChipDefaults.filterChipBorder( enabled = true, selected = isSelected, selectedBorderColor = MaterialTheme.colorScheme.primary, selectedBorderWidth = 1.5.dp ) )
- 使用
7.2 阳光与眩光下的可读性标准
- 主文本 vs. 表面: 在暗色模式下规范化高对比度文本(
onSurface#E1E2E8或#F8FAFC在#14181E上),达到 $\ge 13:1$ 对比度(超过WCAG AAA)。 - 表单控件轮廓:
OutlinedTextField和单选/复选框的边框必须使用outline令牌,使其与容器卡片(Color(0xFF8D9199)或 显式的rgba(255, 255, 255, 0.20))保持至少 $3:1$ 对比度。 - 紧凑预设行: 当布局紧凑的Chip行(例如保留预设
1h、4h、12h、1d)时,保留目标尺寸并应用maxLines = 1和selectedBorderColor,使Chip永不换行或显得模糊。
8. 参考开源仓库
查阅这些行业领先的开源Android项目以获取设计和实现灵感:
android/nowinandroid:Google的旗舰Material 3 Compose示例,演示动态主题、自适应布局和整洁架构。skydoves/Pokedex-Compose:生产级别的共享元素转场(.sharedBounds/.sharedElement)和平滑详情弹窗手势。plainhub/plain-app:本地电话管理、Material You 主题和响应式客户端-服务器同步的杰出实现。Gurupreet/ComposeCookBook:详尽的Compose动画、底部栏和自定义Canvas微交互目录。signalapp/Signal-Android:本地加密存储、后台发件箱工作器和网络弹性的黄金标准。