Android 與行動 UI/UX 工程指南
一套實用、經過實戰考驗的操作框架,用於設計和建構採用 Jetpack Compose、Material 3、離線優先韌性以及以人為本的行動人體工學的原生 Android 應用程式。
1. 核心行動人體工學與拇指區
行動裝置超過 70% 的時間是單手持握。設計介面時應考量拇指可及範圍、觸覺回饋以及最小的物理摩擦。
1.1 人體工學可及性層級
+---------------------------+ <- 螢幕頂部 (僅供顯示)
| 狀態列與標題 | 最難觸及。保留給被動狀態、
| 標題、配額儀表 | 搜尋列和即時儀表。
+---------------------------+
| | <- 中間可及區
| 可滾動內容 | 適合列表項目、詳細資料和
| 收件匣卡片、片段 | 可瀏覽的內容卡片。
| |
+---------------------------+
| 主要操作目標 | <- 主要拇指區 (底部 40%)
| 發送按鈕、標籤頁 | 最容易點擊。將導航、行動呼籲
| 底部工作表、篩選器 | 和互動控制項保留在此處。
+---------------------------+
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 離線優先 Outbox 模式
絕不因行動裝置網路連線不穩或暫時處於飛航模式而導致使用者操作失敗。
flowchart TD
UserAction["使用者分享檔案 / 輸入筆記"] --> LocalDraft["1. 儲存至本地 Outbox 草稿"]
LocalDraft --> RenderOutbox["2. 立即在 Outbox 佇列卡片中呈現"]
RenderOutbox --> CheckNet{"已連線至網路?"}
CheckNet -- 是 --> DirectUpload["3. 直接上傳"]
CheckNet -- 否 --> EnqueueWork["3. 排入 WorkManager Job (NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. 等待 Wi-Fi / 行動網路"]
WaitConn --> AutoDispatch["5. 背景上傳並搭配指數退避"]
DirectUpload --> Success["6. 更新 UI 狀態並顯示 Toast 通知"]
AutoDispatch --> Success
Outbox 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 |
| 檔案 / 媒體 | 下載到資料夾 | 開啟檔案 | Storage Access Framework (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:QR Code 相機掃描 (1 秒配對)
- 在桌面網頁應用程式上顯示一個臨時、已簽署的配對 QR Code。
- 行動使用者開啟應用程式,點擊 掃描 QR Code,將相機對準螢幕。
- 應用程式一次性解析來源、令牌和裝置名稱—立即完成驗證。
- 優先權 2:使用者名稱與密碼 (附可折疊伺服器 URL)
- 99% 的登入使用預設設定的主機。
- 預設僅顯示 使用者名稱 和 密碼 欄位。
- 將伺服器 URL 隱藏在一個細微的可折疊切換按鈕後方:
「伺服器設定 (進階)」。
5.2 14 天滑動式連線階段持續性
- 將存取 (
va1_) 和重新整理 (vr1_) 令牌儲存在 AndroidEncryptedSharedPreferences中。 - 在使用者活躍使用時自動滑動/輪換重新整理令牌。
- 只要使用者在 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 對比度。在低亮度或眩光環境下,使用者無法分辨哪個晶片是啟用的。 - 修正方式:
- 使用
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令牌,以維持與容器卡片至少 $3:1$ 的對比度 (Color(0xFF8D9199)或明確的rgba(255, 255, 255, 0.20))。 - 緊湊預設列: 在佈局緊湊的晶片列時(例如保留預設
1h、4h、12h、1d),保留目標大小並套用maxLines = 1和selectedBorderColor,使晶片永遠不會換行或看起來模糊不清。
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:本地加密儲存、背景 Outbox 工作者和網路韌性的黃金標準。