关于 mobile-principles
面向移动端 Web、iOS 和 Android 开发的触控优先设计技能,提供触控目标、安全区域、手势、无障碍和性能方面的实践规则。适合 Claude Code、claude.ai 和 Cowork 工作流。
主要功能
- 01触控目标尺寸与间距建议
- 02触屏交互与悬停处理
- 03拇指可达区域布局规则
- 04安全区域与系统边距处理
- 05减少动态效果的跨平台方法
- 06手势和性能实践指南
适用场景
- 01检查移动界面的触控可用性
- 02设计适配刘海与系统栏的布局
- 03为动画添加减少动态效果支持
- 04选择移动端常见手势交互
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ athevon/ genjutsu --skill mobile-principles安装后在 Claude Code 中输入/mobile-principles确认已加载。
详细介绍
版本敏感。 以下每个 API 名称、SDK 门槛和浏览器支持声明均已于 2026-09-08 对照一手来源核实。核对了哪些内容,以及核对时间,见
_jutsu/VERSIONS.md。如果该日期已久,请在依据版本号采取行动前重新核实。
移动端原则
触控优先的 UX 背景信息。检测到移动端(移动版 Web、iOS、Android)时加载。 此处列出简明规则。深入内容见
references/。
触控目标
| 平台 | 推荐尺寸(按此设计) | 硬性下限 | 间距 | 规范 |
|---|---|---|---|---|
| Apple iOS / iPadOS | 44x44 pt | 28x28 pt | 带边框控件周围约 12 pt 内边距,无边框控件周围约 24 pt | Apple HIG,辅助功能 > 移动性 |
| Apple watchOS | 44x44 pt | 28x28 pt | 同上 | Apple HIG |
| Android | 48x48 dp | 48x48 dp | 目标之间间隔 8 dp | Material Design 3 |
| Web 移动端 | 44x44 CSS px | 24x24 CSS px | 不重叠的间距为 24 px 时,符合 AA 例外要求 | WCAG 2.5.5 目标尺寸(增强),AAA = 44x44;WCAG 2.5.8 目标尺寸(最低),AA = 24x24 |
**经验法则:**任何小于平台最低尺寸的点击目标都属于可用性缺陷,仅此而已。
命中区域可以超出可见图形(使用内边距、hitSlop 或透明的内部间隔元素),但可交互表面必须达到最低尺寸。间距和尺寸同样重要:两个边缘相接的 44pt 按钮仍然很容易误触。
不依赖悬停原则
触控设备上不存在 :hover。将其作为主要触发方式,意味着每部手机上都会有隐藏的操作入口。任何只能通过悬停触发的功能,在移动端都等于消失。默认显示是原则;悬停样式只能作为桌面端的增强,绝不能成为关键交互。
CSS - 将悬停样式限制在媒体查询中:
.card { opacity: 1; transform: translateY(0); }
@media (hover: hover) and (pointer: fine) {
.card { opacity: 0.85; }
.card:hover { opacity: 1; transform: translateY(-2px); }
}
SwiftUI - 使用轻点和长按,不使用伪悬停:
Image(systemName: "heart")
.onTapGesture { toggleLike() }
.contextMenu {
Button("Share", systemImage: "square.and.arrow.up", action: share)
Button("Report", systemImage: "flag", role: .destructive, action: report)
}
Compose - 使用 combinedClickable 处理轻点和长按:
Box(
modifier = Modifier
.combinedClickable(
onClick = { toggleLike() },
onLongClick = { showContextMenu() },
)
) {
Icon(Icons.Default.Favorite, contentDescription = "Like")
}
拇指区域(Hoober)
Steven Hoober 的研究表明,用户通常单手或将手机托在手中使用竖屏手机,拇指以底部角落为支点活动。屏幕可划分为以下可触及区域:
+------+----+------+
| HARD | OK | HARD | <- top: stretch, two-handed only
+------+----+------+
| OK | OK | OK | <- middle: comfortable
+------+----+------+
| EASY |EASY| EASY | <- bottom: natural thumb arc
+------+----+------+
- **底部三分之一(EASY):**主要 CTA、发送、确认、FAB、标签栏。
- **中间(OK):**内容、次要操作。
- **顶部(HARD):**返回、关闭、搜索、个人资料——用户预期会主动去点,而不会下意识点击的内容。
**规则:**主要 CTA 放在下半屏。次要、不常用或具有破坏性的操作放在顶部。绝不要把“付款”按钮放在手机屏幕的右上角。
安全区域
| 平台 | API | 需要考虑的内边距 |
|---|---|---|
| Web | `env(safe-area-inset-top | right |
| SwiftUI | .safeAreaInset(edge: ...)、safeAreaInsets 环境变量 | 导航栏、标签栏、刘海、主屏幕 |
| Compose | Modifier.windowInsetsPadding(WindowInsets.safeDrawing) | 系统栏、IME、挖孔区域 |
Web:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
.fab {
position: fixed;
bottom: calc(env(safe-area-inset-bottom) + 16px);
right: calc(env(safe-area-inset-right) + 16px);
}
SwiftUI:
ScrollView { content }
.safeAreaInset(edge: .bottom) {
PrimaryCTA().padding()
}
Compose:
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
) { /* content */ }
减少动态效果(跨平台统一)
| 平台 | API |
|---|---|
| Web CSS | @media (prefers-reduced-motion: reduce) |
| Web JS | window.matchMedia('(prefers-reduced-motion: reduce)') |
| SwiftUI | @Environment(\.accessibilityReduceMotion) var reduceMotion |
| UIKit | UIAccessibility.isReduceMotionEnabled |
| Compose | 使用 Settings.Global.ANIMATOR_DURATION_SCALE 的自定义辅助函数(见下方代码;深入内容见 references/accessibility-mobile.md) |
SwiftUI:
struct Hero: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
@State private var shown = false
var body: some View {
Text("Welcome")
.opacity(shown ? 1 : 0)
.offset(y: shown ? 0 : (reduceMotion ? 0 : 20))
.animation(reduceMotion ? .none : .easeOut(duration: 0.3), value: shown)
.onAppear { shown = true }
}
}
UIKit:
let duration = UIAccessibility.isReduceMotionEnabled ? 0 : 0.3
UIView.animate(withDuration: duration) {
view.alpha = 1
view.transform = .identity
}
Compose(辅助函数模式):
@Composable
fun rememberReduceMotion(): Boolean {
val context = LocalContext.current
return remember {
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
) == 0f
}
}
val reduceMotion = rememberReduceMotion()
val spec = if (reduceMotion) snap() else tween<Float>(durationMillis = 300)
注意:优先使用
ValueAnimator.areAnimatorsEnabled()(API 26+)——当动画时长缩放为 0 时,它会返回false。开发者选项中的“关闭动画”开关、“省电模式”以及面向用户的“移除动画”(设置 -> 辅助功能)开关都会将其设为 0。深入内容见references/accessibility-mobile.md。
移动端手势(规范模式)
用户已经熟悉的五种手势。重用它们无需额外 UX 成本;重新发明只会增加阻力。
- 边缘右滑返回: 在 iOS 上从屏幕起始边缘向内滑动,以弹出导航堆栈。绝不要覆盖此手势;在 Android 上通过预测性返回与之保持一致。该功能从 Android 13 (API 33) 起提供,需通过
android:enableOnBackInvokedCallback="true"和OnBackPressedCallback/PredictiveBackHandler启用。在 Android 13-14 上,这些动画需通过开发者选项开启;从 Android 15 起,启用该功能的应用会自动显示系统返回主屏幕、跨 Activity 和跨任务动画。 - 下拉刷新: 在滚动区域顶部向下拖动以重新获取数据。信息流、邮件和列表的标准手势。
- 下拖关闭: 向下拖动模态表单和图片查看器,超过阈值(通常为 100-150pt)时将其关闭。
- 双指缩放: 在图片、地图和可缩放画布上,用两根手指张开或捏合。遵守最小/最大缩放比例。
- 列表项滑动操作: 在列表项上水平滑动,以显示上下文操作(删除、归档、标记为已读)。起始侧滑动和末尾侧滑动对应不同的操作集。
移动端性能预算
- 冷启动: 在中端设备上低于 2s。基准设备:Android Pixel 4a、iPhone SE(第 2 代)。如果应用在 Pixel 4a 上耗时 4s,那么在用户实际拥有的低端设备上就会耗时 8s。
- 帧预算: 60fps 时每帧 16.67ms,120fps 时每帧 8.33ms(ProMotion / 高刷新率 Android)。主线程上任何超过此时间的同步操作都会造成卡顿。
- 二进制大小: 在添加大型媒体库之前,目标是 APK 小于 30MB、IPA 小于 50MB。Lottie/Rive 会增加 500KB 到 2MB。留意资源文件夹;通常罪魁祸首是未使用 WebP / 矢量格式的 PNG。
- 电池: 后台不要持续占用 CPU。合并任务,使用平台调度器(Android 上的
WorkManager、iOS 上的BGTaskScheduler);除非用户明确要求前台媒体播放,否则避免使用唤醒锁。 - 网络: 遵循连接提示。Web:
Save-Data请求标头和navigator.connection.saveData。iOS:URLSessionConfiguration.allowsCellularAccess和NWPathMonitor,用于区分蜂窝网络与 Wi-Fi。Android:使用ConnectivityManager+NetworkCapabilities检测按流量计费的网络。
反模式(错误 / 正确)
1. 仅通过悬停显示内容
/* BAD - on mobile the action button literally never appears */
.card .actions { opacity: 0; }
.card:hover .actions { opacity: 1; }
/* GOOD - visible by default, hover is a desktop enhancement */
.card .actions { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
.card .actions { opacity: 0; transition: opacity 0.15s ease-out; }
.card:hover .actions { opacity: 1; }
}
2. 触控目标小于最小尺寸
// BAD - looks 32dp and reads as a 32dp target to whoever writes the next one.
// Material 3's IconButton actually applies minimumInteractiveComponentSize() itself, so
// the touch target stays 48dp here - but only because the component rescues you, and only
// while LocalMinimumInteractiveComponentEnforcement is on. On a custom Box or Row with the
// same modifier, the target really is 32dp and really is mistappable.
IconButton(
onClick = onDelete,
modifier = Modifier.size(32.dp),
) { Icon(Icons.Default.Delete, contentDescription = "Delete") }
// GOOD - 48dp minimum even when the visible icon is 24dp
IconButton(
onClick = onDelete,
modifier = Modifier.size(48.dp),
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
modifier = Modifier.size(24.dp),
)
}
3. 忽略安全区域内边距
// BAD - the CTA sits under the home indicator on every modern iPhone
VStack {
Spacer()
Button("Continue", action: next)
.frame(maxWidth: .infinity)
.padding()
}
// GOOD - safeAreaInset keeps the button reachable and visible
ScrollView { content }
.safeAreaInset(edge: .bottom) {
Button("Continue", action: next)
.frame(maxWidth: .infinity)
.padding()
}
快速参考:加载子技能
| 需求 | 加载 |
|---|---|
| 手势深入解析 | references/gestures-deep.md |
| 移动端无障碍深入解析 | references/accessibility-mobile.md |
| Compose 专用动画 | ../compose-motion/SKILL.md |
| SwiftUI 专用动画 | ../swiftui-motion/SKILL.md |
来源
- Steven Hoober,“用户究竟如何握持移动设备?”,UXmatters,2013 年 2 月 18 日(观察人数 n=1,333:49% 单手握持、36% 托持、15% 双手握持):https://www.uxmatters.com/mt/archives/2013/02/how-do-users-really-hold-mobile-devices.php
- Apple 人机界面指南(iOS):https://developer.apple.com/design/human-interface-guidelines/
- Material Design(Android):https://m3.material.io/foundations/layout/canonical-layouts/overview
- WCAG 2.5.8 目标尺寸(最小),AA 级,24x24 CSS px:https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html
- WCAG 2.5.5 目标尺寸(增强),AAA 级,44x44 CSS px:https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html