E-Ink 設計系統:為什麼我的 App 故意不做動畫
一個關於「拿掉」的設計決定
路順是一款外勤業務用的路線規劃 App。開發初期,我用了 SwiftUI 預設的樣式:圓角卡片、藍色按鈕、滑入滑出的轉場動畫。看起來跟市面上大多數 App 差不多,功能正常,但沒有任何辨識度。
我想讓路順看起來不一樣。但不是那種「配色大膽」或「加更多動效」的不一樣,而是反過來:把東西拿掉。
最後定下來的方向是電子紙(E-Ink)風格。灰階畫面、無圓角、無平滑動畫、點陣字體,整個 App 看起來像一台 Kindle 或早期的 Apple II 終端機。全畫面只有一個顏色:螢光綠,用在最重要的那顆按鈕上。
設計系統的四條規則
我在 EInkTheme.swift 的開頭寫了四條規則,之後所有的設計決定都從這四條出發:
- 沒有平滑動畫。電子紙的畫面是「刷新」,不是「移動」。
- 灰階為主。螢光綠只用在一個重點元素上。
- 換頁或內容變動時閃一下(refresh flash),搭配觸覺回饋。
- 地圖灰階化。點擊後用原生 modal 喚醒全彩地圖。
這四條規則聽起來都是在「限制」,但每一條都讓後續的設計決定變得更快。當你知道不能做什麼的時候,剩下能做的選擇就變少了,而少的選擇往往做得更好。
灰階色票:五個顏色撐起整個 App
路順的 UI 只用了五個顏色:
| 名稱 | 色碼 | 用途 |
|---|---|---|
| Paper 紙白 | #F4F1EA | 背景。不是純白,帶一點暖黃,模擬紙張質感 |
| Ink 墨黑 | #1A1A1A | 主要文字、邊框、標題 |
| Ink Light 淡墨 | 40% 灰 | 次要文字 |
| Ghost 殘影灰 | 80% 灰 | 分隔線、卡片邊框(像電子紙殘影) |
| Phosphor 螢光綠 | #33FF33 | 唯一的彩色,只用在主要行動按鈕 |
背景色選 #F4F1EA 而非純白,是因為真正的電子紙螢幕不是白色的,而是帶著一點米黃。這個微小的色差讓整個介面從「黑白 App」變成「像紙的 App」。
螢光綠只出現在一個地方:主要操作按鈕(例如「開始最佳化路線」)。當整個畫面都是灰階的時候,那一顆綠色按鈕自然就成為視覺焦點,不需要放大字體或加箭頭來引導使用者的注意力。
enum EInk {
enum Colors {
static let paper = Color(red: 0.957, green: 0.945, blue: 0.918)
static let ink = Color(red: 0.102, green: 0.102, blue: 0.102)
static let inkLight = Color(red: 0.42, green: 0.42, blue: 0.40)
static let ghost = Color(red: 0.80, green: 0.79, blue: 0.76)
static let phosphor = Color(red: 0.20, green: 1.0, blue: 0.20)
}
}
拿掉圓角
SwiftUI 的預設圓角半徑很好看,柔和、現代、親切。但圓角也讓所有 SwiftUI App 長得差不多。
路順把所有圓角設成 0。卡片是方的,按鈕是方的,輸入框也是方的。配上邊框線條,整個介面看起來像報紙版面或印刷標籤。
enum AppMetrics {
static let cardCornerRadius: CGFloat = 0
static let iconTileCornerRadius: CGFloat = 0
}
卡片元件在左側加了一條細墨條,模擬打印標籤的效果:
struct EInkCard: ViewModifier {
func body(content: Content) -> some View {
content
.padding(16)
.background(EInk.Colors.paper)
.overlay(
Rectangle().strokeBorder(EInk.Colors.ghost, lineWidth: 1)
)
.overlay(alignment: .leading) {
Rectangle().fill(EInk.Colors.ink).frame(width: 3)
}
}
}
用「刷新」取代「動畫」
這是整個設計系統裡最核心的決定。
SwiftUI 預設的轉場動畫是滑入滑出,清單項目的增減有彈性動畫,狀態變化有淡入淡出。這些動畫讓介面感覺流暢,但也讓介面感覺像「液態」的。電子紙的畫面更新方式完全不同:舊畫面消失,螢幕閃黑一下,新畫面出現。是離散的,不是連續的。
我用一行程式碼關掉了所有隱式動畫:
.transaction { $0.animation = nil }
然後用 EInkRefreshController 做「刷新閃爍」效果:畫面疊上一層淡墨色(透明度只有 12%),持續 0.12 秒,配合一個 rigid 觸覺回饋。使用者看到的效果是:按下按鈕後,畫面微微閃了一下,內容就已經換好了。
@Observable
final class EInkRefreshController {
var isFlashing = false
@MainActor
func trigger(haptic: Bool = true) async {
if haptic {
UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
}
isFlashing = true
try? await Task.sleep(for: .seconds(0.12))
isFlashing = false
}
}
早期版本的閃爍效果更強烈,整面蓋上墨黑色。測試時使用者反映太突兀,有被嚇到的感覺,所以改成淡入淡出的微暈染:淡入 0.1 秒、淡出 0.25 秒,透明度壓低到 12%。這是整個「無動畫」原則裡唯一的例外,而且是使用者反饋逼出來的例外。
紙張顆粒質感
為了讓背景更像紙張,我在畫面最上層蓋了一個 PaperGrain 元件。它用 Canvas 在畫面上隨機撒了一些幾乎看不見的點(透明度 5%),模擬紙張的纖維顆粒。
這裡有一個細節:隨機數用的是固定 seed 的偽隨機產生器,而非系統的隨機函數。這樣每次畫面重繪時,顆粒的位置不會變動。如果用系統隨機數,每次 SwiftUI 重繪這個 View,顆粒會跳到新位置,畫面會「閃」。
private struct SeededRandom {
var state: UInt64
init(seed: UInt64) { state = seed }
mutating func next() -> CGFloat {
state = state &* 6364136223846793005 &+ 1442695040888963407
return CGFloat(state % 10_000) / 10_000
}
}
灰階地圖與「喚醒」
路順是路線規劃 App,地圖是核心功能。但全彩地圖放在灰階介面裡,視覺上非常突兀,像在報紙上貼了一張彩色照片。
解法是把地圖也灰階化。SwiftUI 的 Map 元件加上三行 modifier 就能做到:
.saturation(0) // 去掉所有色彩
.contrast(1.15) // 提高對比度
.brightness(0.03) // 稍微提亮
灰階地圖右下角放了一個「TAP TO WAKE ►」的標籤,用點陣字體、白字黑底。使用者點擊灰階地圖後,一個全彩的原生地圖從底部滑出來。從灰色紙張世界切換到全彩地圖,這個反差很有趣:灰階讓全彩變得更有價值。
按鈕的兩種狀態
路順有兩種按鈕樣式。主要按鈕用螢光綠:深底綠框、綠色點陣字。按下去的時候,前景和背景顏色對調(綠底黑字變成黑底綠字),同時觸發一個重量級的觸覺回饋。
次要按鈕用墨線框:白底黑框黑字,按下去變成黑底白字。整個互動都是瞬間切換,沒有漸變。
早期我試過用 SwiftUI 的 .colorInvert() 來做按下時的反色效果,但 #33FF33 反轉出來是紫紅色,視覺上很突兀。最後改成手動指定按下狀態的前景和背景顏色。
struct PhosphorButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
let pressed = configuration.isPressed
return configuration.label
.font(EInk.Fonts.dotMatrix(14))
.tracking(1)
.foregroundStyle(pressed ? EInk.Colors.phosphorBg : EInk.Colors.phosphor)
.padding(.vertical, 12)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity)
.background(pressed ? EInk.Colors.phosphor : EInk.Colors.phosphorBg)
.einkInstant()
.sensoryFeedback(.impact(weight: .heavy), trigger: configuration.isPressed)
}
}
為什麼這套設計系統有效
路順開發完後,給多個業務朋友使用,使用者對介面的反應超出預期。好幾則回饋特別提到了「風格很特別」和「看起來很舒服」。一個外勤業務工具能讓人注意到視覺設計,這在同類 App 裡很少見。
回頭看,我覺得這套設計系統有效的原因有兩個。
第一,限制產生辨識度。當你把可用的顏色壓到五個、把圓角拿掉、把動畫關掉,剩下來的每一個視覺元素都必須有明確的功能。螢光綠不是裝飾,它是「這裡可以按」的訊號。刷新閃爍不是炫技,它是「內容已經更新」的提示。每個視覺元素都在做事,沒有純裝飾的成分。
第二,反差創造記憶點。灰階讓螢光綠更亮,無動畫讓刷新閃爍更有存在感,灰階地圖讓全彩 modal 更驚喜。拿掉的東西反而讓留下來的東西更突出。
路順(RouteSync)是一款為外勤業務設計的路線規劃工具,支援繁體中文、英文與日文。
費曼的實習生系列:用簡單的話,記錄不簡單的開發過程。