mobile-principles

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mobile Principles

移动端设计原则

Touch-first UX context. Loaded when mobile is detected (web mobile, iOS, Android). Concise rules here. Deep-dive in
references/
.

以触摸优先的UX设计场景。检测到移动端(移动网页、iOS、Android)时加载。 此处为精简规则,详细内容见
references/
目录。

Touch Targets

触摸目标

PlatformMinimumRecommendedSpec
Apple iOS44pt44pt + 8pt spacingApple HIG
Android48dp48dp + 8dp spacingMaterial Design
Web mobile44px44px + 8px spacingWCAG 2.5.5
Rule of thumb: any tap target smaller than the platform minimum is a usability bug, period. The hit area can extend beyond the visible glyph (use padding,
hitSlop
, or a transparent inner spacer), but the interactive surface must reach the minimum. Spacing matters as much as size: two 44pt buttons touching edges are still mistappable.

平台最小值推荐值规范来源
Apple iOS44pt44pt + 8pt 间距Apple HIG
Android48dp48dp + 8dp 间距Material Design
移动网页44px44px + 8px 间距WCAG 2.5.5
**经验法则:**任何小于平台最小值的点击目标都是可用性缺陷,没有例外。 点击热区可以超出可见图标范围(使用内边距、
hitSlop
或透明内部间隔元素),但交互区域必须达到最小值。间距和尺寸同样重要:两个边缘紧贴的44pt按钮仍然容易误触。

No-Hover Doctrine

无悬停准则

:hover
does not exist on touch. Treating it as a primary trigger means hidden affordances on every phone. Anything reachable only by hover is, on mobile, simply gone. Visible-by-default is the rule; hover styles are a desktop enhancement, never a load-bearing interaction.
CSS - gate hover styles behind a media query:
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 - tap and long-press, no pseudo-hover:
swift
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 for tap + long-press:
kotlin
Box(
  modifier = Modifier
    .combinedClickable(
      onClick = { toggleLike() },
      onLongClick = { showContextMenu() },
    )
) {
  Icon(Icons.Default.Favorite, contentDescription = "Like")
}

:hover
在触摸设备上不存在。将其作为主要触发方式意味着在手机上会出现隐藏的交互入口。任何只能通过悬停触发的功能,在移动端相当于完全不可用。默认可见是核心规则;悬停样式只是桌面端的增强效果,绝不能作为核心交互的承载方式。
CSS - 通过媒体查询控制悬停样式:
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 - 使用点击和长按,避免伪悬停:
swift
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实现点击+长按:
kotlin
Box(
  modifier = Modifier
    .combinedClickable(
      onClick = { toggleLike() },
      onLongClick = { showContextMenu() },
    )
) {
  Icon(Icons.Default.Favorite, contentDescription = "Like")
}

Thumb Zones (Hoober)

拇指操作区域(Hoober研究)

Steven Hoober's research shows portrait phone use is overwhelmingly one-handed or cradled, with the thumb pivoting from the bottom corner. The screen splits into reachable zones:
+------+----+------+
| HARD | OK | HARD |   <- top: stretch, two-handed only
+------+----+------+
|  OK  | OK |  OK  |   <- middle: comfortable
+------+----+------+
| EASY |EASY| EASY |   <- bottom: natural thumb arc
+------+----+------+
  • Bottom third (EASY): primary CTA, send, confirm, FAB, tab bar.
  • Middle (OK): content, secondary actions.
  • Top (HARD): back, close, search, profile - things the user expects to reach for, not hit by reflex.
Rule: primary CTA goes in the bottom half. Secondary, less-frequent or destructive actions go in the top. Never put a "Pay" button in the top-right corner of a phone screen.

Steven Hoober的研究表明,竖屏手机的使用场景绝大多数是单手握持或托握,拇指从底部角落转动操作。屏幕可划分为不同的可触及区域:
+------+----+------+
| 难触及 | 可触及 | 难触及 |   <- 顶部:需伸展手指,仅适合双手操作
+------+----+------+
| 可触及 | 可触及 | 可触及 |   <- 中部:操作舒适
+------+----+------+
| 易触及 | 易触及 | 易触及 |   <- 底部:拇指自然活动范围
+------+----+------+
  • **底部三分之一(易触及):**主要CTA、发送、确认、FAB、标签栏。
  • **中部(可触及):**内容区域、次要操作。
  • **顶部(难触及):**返回、关闭、搜索、个人资料——用户预期需要主动伸手操作的功能,而非下意识点击的按钮。
**规则:**主要CTA应放在屏幕下半部分。次要、低频率或破坏性操作放在顶部。绝不要将“支付”按钮放在手机屏幕的右上角。

Safe Areas

安全区域

PlatformAPIInsets respected
Web`env(safe-area-inset-topright
SwiftUI
.safeAreaInset(edge: ...)
,
safeAreaInsets
env
nav bar, tab bar, notch, home
Compose
Modifier.windowInsetsPadding(WindowInsets.safeDrawing)
system bars, IME, cutouts
Web:
html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
css
.fab {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom) + 16px);
  right: calc(env(safe-area-inset-right) + 16px);
}
SwiftUI:
swift
ScrollView { content }
  .safeAreaInset(edge: .bottom) {
    PrimaryCTA().padding()
  }
Compose:
kotlin
Column(
  modifier = Modifier
    .fillMaxSize()
    .windowInsetsPadding(WindowInsets.safeDrawing)
) { /* content */ }

平台API需适配的内边距
网页`env(safe-area-inset-topright
SwiftUI
.safeAreaInset(edge: ...)
safeAreaInsets
环境变量
导航栏、标签栏、刘海屏、Home指示器
Compose
Modifier.windowInsetsPadding(WindowInsets.safeDrawing)
系统状态栏、输入法、屏幕切口
网页端:
html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
css
.fab {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom) + 16px);
  right: calc(env(safe-area-inset-right) + 16px);
}
SwiftUI:
swift
ScrollView { content }
  .safeAreaInset(edge: .bottom) {
    PrimaryCTA().padding()
  }
Compose:
kotlin
Column(
  modifier = Modifier
    .fillMaxSize()
    .windowInsetsPadding(WindowInsets.safeDrawing)
) { /* content */ }

Reduced Motion (cross-platform unified)

减少动效(跨平台统一实现)

PlatformAPI
Web CSS
@media (prefers-reduced-motion: reduce)
Web JS
window.matchMedia('(prefers-reduced-motion: reduce)')
SwiftUI
@Environment(\.accessibilityReduceMotion) var reduceMotion
UIKit
UIAccessibility.isReduceMotionEnabled
ComposeCustom helper using
Settings.Global.ANIMATOR_DURATION_SCALE
(see code below; deep-dive in
references/accessibility-mobile.md
)
SwiftUI:
swift
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:
swift
let duration = UIAccessibility.isReduceMotionEnabled ? 0 : 0.3
UIView.animate(withDuration: duration) {
  view.alpha = 1
  view.transform = .identity
}
Compose (helper pattern):
kotlin
@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)
Note: prefer
ValueAnimator.areAnimatorsEnabled()
(API 26+) - it returns
false
when the animator duration scale is 0, which the developer-options "Animation off" toggle, Battery Saver, and the user-facing "Remove animations" (Settings -> Accessibility) toggle all set. Deep dive in
references/accessibility-mobile.md
.

平台API
网页CSS
@media (prefers-reduced-motion: reduce)
网页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:
swift
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:
swift
let duration = UIAccessibility.isReduceMotionEnabled ? 0 : 0.3
UIView.animate(withDuration: duration) {
  view.alpha = 1
  view.transform = .identity
}
Compose(工具类模式):
kotlin
@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
,开发者选项中的“关闭动画”开关、省电模式以及用户可见的“移除动画”(设置 -> 无障碍)开关都会触发这个状态。详细内容见
references/accessibility-mobile.md

Mobile Gestures (canonical patterns)

移动端手势(标准模式)

The five gestures users already know. Reusing them is free UX; reinventing them is friction.
  • Swipe-back: iOS edge-swipe from the left to pop the navigation stack. Never override; mirror it on Android via predictive back (Android 14+).
  • Pull-to-refresh: downward drag at the top of a scroll surface to refetch. Standard on feeds, mail, lists.
  • Drag-to-dismiss: modal sheets and image viewers close when dragged downward past a threshold (typically 100-150pt).
  • Pinch-to-zoom: two-finger spread/pinch on images, maps, and zoomable canvases. Respect minimum/maximum scale.
  • Swipe actions on rows: horizontal swipe on a list row to reveal contextual actions (delete, archive, mark read). Leading vs trailing swipe = different action sets.

用户已经熟知的五种手势。复用这些手势能获得天然的UX体验;重新发明手势则会增加操作摩擦。
  • **侧滑返回:**iOS从左侧边缘滑动返回上一页。绝不要重写此功能;在Android 14+上通过预测性返回实现类似效果。
  • **下拉刷新:**在滚动区域顶部向下拖动以重新加载内容。在信息流、邮件、列表中是标准操作。
  • **拖动关闭:**模态弹窗和图片查看器向下拖动超过阈值(通常为100-150pt)时关闭。
  • **双指缩放:**在图片、地图和可缩放画布上双指张开/捏合实现缩放。需遵循最小/最大缩放比例。
  • **列表行滑动操作:**在列表行上横向滑动以显示上下文操作(删除、归档、标记已读)。左滑和右滑对应不同的操作集合。

Mobile Performance Budgets

移动端性能预算

  • Cold start: <2s on mid-range devices. Baselines: Android Pixel 4a, iPhone SE (2nd gen). If your app takes 4s on a Pixel 4a, it takes 8s on a low-end device users actually own.
  • Frame budget: 16.67ms per frame at 60fps, 8.33ms at 120fps (ProMotion / high-refresh Android). Anything synchronous on the main thread above that = jank.
  • Binary size: target <30MB APK and <50MB IPA before adding heavy media libs. Lottie/Rive add 500KB to 2MB. Watch your asset folders; PNGs over WebP / vector are the usual culprit.
  • Battery: no continuous CPU activity in the background. Coalesce work, use platform schedulers (
    WorkManager
    on Android,
    BGTaskScheduler
    on iOS), avoid wake-locks unless the user explicitly asked for foreground media.
  • Network: respect connection hints. Web:
    Save-Data
    request header and
    navigator.connection.saveData
    . iOS:
    URLSessionConfiguration.allowsCellularAccess
    and
    NWPathMonitor
    for cellular vs Wi-Fi. Android:
    ConnectivityManager
    +
    NetworkCapabilities
    to detect metered networks.

  • **冷启动:**中端设备上耗时<2秒。基准设备:Android Pixel 4a、iPhone SE(第二代)。如果你的应用在Pixel 4a上启动需要4秒,那么在用户实际使用的低端设备上会需要8秒。
  • **帧预算:**60fps下每帧耗时<16.67毫秒,120fps(ProMotion / 高刷新率Android)下每帧耗时<8.33毫秒。主线程上任何超过此时间的同步操作都会导致卡顿。
  • **安装包大小:**在添加重型媒体库前,目标APK<30MB,IPA<50MB。Lottie/Rive会增加500KB到2MB的体积。注意资源文件夹;PNG格式比WebP/矢量图更占体积是常见问题。
  • **电池消耗:**后台不要持续占用CPU。合并任务,使用平台调度器(Android的
    WorkManager
    、iOS的
    BGTaskScheduler
    ),除非用户明确要求前台媒体播放,否则避免使用唤醒锁。
  • **网络适配:**遵循网络连接提示。网页端:
    Save-Data
    请求头和
    navigator.connection.saveData
    。iOS:
    URLSessionConfiguration.allowsCellularAccess
    NWPathMonitor
    区分蜂窝网络与Wi-Fi。Android:
    ConnectivityManager
    +
    NetworkCapabilities
    检测计量网络。

Anti-Patterns (BAD / GOOD)

反模式(错误示例 / 正确示例)

1. Hover as the only reveal

1. 仅通过悬停显示操作按钮

css
/* BAD - on mobile the action button literally never appears */
.card .actions { opacity: 0; }
.card:hover .actions { opacity: 1; }
css
/* 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; }
}
css
/* 错误示例 - 在移动端操作按钮完全不会显示 */
.card .actions { opacity: 0; }
.card:hover .actions { opacity: 1; }
css
/* 正确示例 - 默认可见,悬停仅作为桌面端增强效果 */
.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. Sub-minimum touch targets

2. 小于最小值的触摸目标

kotlin
// BAD - 32dp icon button, mistappable, fails Material guideline
IconButton(
  onClick = onDelete,
  modifier = Modifier.size(32.dp),
) { Icon(Icons.Default.Delete, contentDescription = "Delete") }
kotlin
// 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),
  )
}
kotlin
// 错误示例 - 32dp的图标按钮,容易误触,不符合Material Design规范
IconButton(
  onClick = onDelete,
  modifier = Modifier.size(32.dp),
) { Icon(Icons.Default.Delete, contentDescription = "Delete") }
kotlin
// 正确示例 - 即使可见图标是24dp,也要保证48dp的最小触摸区域
IconButton(
  onClick = onDelete,
  modifier = Modifier.size(48.dp),
) {
  Icon(
    Icons.Default.Delete,
    contentDescription = "Delete",
    modifier = Modifier.size(24.dp),
  )
}

3. Ignoring safe area insets

3. 忽略安全区域内边距

swift
// BAD - the CTA sits under the home indicator on every modern iPhone
VStack {
  Spacer()
  Button("Continue", action: next)
    .frame(maxWidth: .infinity)
    .padding()
}
swift
// GOOD - safeAreaInset keeps the button reachable and visible
ScrollView { content }
  .safeAreaInset(edge: .bottom) {
    Button("Continue", action: next)
      .frame(maxWidth: .infinity)
      .padding()
  }

swift
// 错误示例 - CTA按钮会被所有现代iPhone的Home指示器遮挡
VStack {
  Spacer()
  Button("Continue", action: next)
    .frame(maxWidth: .infinity)
    .padding()
}
swift
// 正确示例 - safeAreaInset确保按钮可触及且可见
ScrollView { content }
  .safeAreaInset(edge: .bottom) {
    Button("Continue", action: next)
      .frame(maxWidth: .infinity)
      .padding()
  }

Quick Reference: Loading sub-skills

快速参考:加载子技能

NeedLoad
Gesture deep-dive
references/gestures-deep.md
Mobile a11y deep-dive
references/accessibility-mobile.md
Compose-specific anim
../compose-motion/SKILL.md
SwiftUI-specific anim
../swiftui-motion/SKILL.md

需求加载路径
手势详细指南
references/gestures-deep.md
移动端无障碍详细指南
references/accessibility-mobile.md
Compose专属动效
../compose-motion/SKILL.md
SwiftUI专属动效
../swiftui-motion/SKILL.md

Sources

参考来源