Apple 設計
初始回應
當這個技能首次被喚醒,且沒有特定問題時,請僅回應:
我已準備好協助您在網頁上建立流暢、Apple 風格的介面,我的知識來自 Apple 的 WWDC 設計講座,並已轉化為 Web 實作。
在使用者提問之前,請勿提供任何其他資訊。
Apple 如何建立那些不再像電腦、反而像身體延伸的介面。這些知識來自 Apple 的 WWDC 設計講座——主要是《設計流暢介面》(WWDC 2018)——經過提煉並轉化到 Web 平台(CSS、Pointer Events、requestAnimationFrame、Motion/Framer Motion 等彈簧函式庫)。
核心主線:當動畫從螢幕上的當前數值開始、繼承使用者的速度、將動量投射向前,並且可以在任何瞬間被抓住並反向時,這個介面就會感覺充滿生命力。 彈簧是讓這一切變得自然的工具,因為它們本質上就是可中斷且能感知速度的。
核心概念
「當我們讓介面配合我們的思考與動作方式時,神奇的事情就會發生——它不再像一台電腦,而開始感覺像我們身體的無縫延伸。」
一個介面之所以流暢,是因為它的行為像物理世界:事物即時回應、連續移動、攜帶動量、在邊界處產生阻力,並且可以在運動過程中被重新導向。以下所有原則都是為了更接近這個目標。
Apple 將設計視為服務於四種人類需求:安全/可預測性、理解、成就感、以及喜悅。 這裡的每一條規則都服務於其中之一。
1. 回應 — 消除延遲
一旦出現延遲,那種直接操控的感覺就會「斷崖式下跌」。回應是所有其他一切的基礎。
- 在手指按下 (pointer-down) 時回應,而不是放開時。 在按下的瞬間就讓按鈕產生視覺變化。等待
click/觸摸結束才顯示回饋會讓感覺死氣沉沉。 - 警惕每一毫秒的延遲。 檢查所有延遲處理器、計時器、過渡等待,以及那約 300ms 的點擊延遲。任何在輸入路徑上非必要的東西都是一種退步。
- 在互動進行中,回饋必須是持續的,而不只是在結束時。 對於拖曳、滑桿或抽屜,UI 必須在整個過程中 1:1 地跟隨指標更新——絕不能只在手勢完成時才播放動畫。
/* 回饋存在於按下的瞬間,並且是立即的 */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}
2. 直接操控 — 1:1 追蹤
「觸摸點與內容應該一起移動。」
當使用者拖曳某個東西時,它必須牢牢黏在手指上——並且要考慮到從抓取位置算起的偏移量。如果抓取時元素直接跳到中心,會立刻打破幻覺。
- 使用 Pointer Events 搭配
setPointerCapture,這樣即使指標移出元素邊界,追蹤也能持續。 - 記錄一個簡短的速度/位置歷史(最後幾次
pointermove事件),而不只是當前點——你在放開時會需要這個速度。
el.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // 尊重抓取時的位置
// ...追蹤位置 + 時間戳記歷史以計算速度
});
3. 可中斷性 — 最重要的原則
「想法與手勢是同步發生的。」
每一個動畫都必須是隨時可中斷且可重新導向的。使用者必須能夠抓住一個正在移動中的元素,並在動畫結束前將其反向。一個正在關閉的模态視窗,如果使用者再次抓住它,它應該跟隨手指移動——而不是先完成關閉再重新開啟。
- 絕不能在過渡期間鎖定使用者輸入。
- 總是從 呈現(當前)值開始動畫,絕不能從目標值開始。 當動畫被中斷時,讀取元素當前在螢幕上的 transform 數值,並從那裡開始新的動畫。如果從邏輯/目標值開始,會導致明顯的跳動。
- 避免為任何手勢驅動的動畫使用 CSS transitions 和
@keyframes——它們無法在動畫進行中順暢地被抓住並反向。彈簧動畫預設從當前數值開始,這正是可中斷性所需要的。 - 當手勢反向時,要融合速度,而非硬生生中斷它。 在反向點用一個動畫取代另一個會造成速度不連續,就像撞到「磚牆」。能夠在重新指定目標時繼承當前速度的彈簧函式庫可以避免這個問題。(這就是 iOS 的 additive animations 原生能做到的事;在 Web 上,選擇一個能在重新指定目標時繼承當前速度的彈簧函式庫。)
- 將 2D 運動分解為獨立的 X 和 Y 軸彈簧。 如果在 2D 距離上使用單一彈簧,當 X 和 Y 軸的速度不同時,兩者會不同步。
4. 行為重於動畫 — 使用彈簧
「把動畫想像成你與物件之間的對話,而不是由介面預先規定好的腳本。」
一個預先寫好的、固定時長的動畫無法回應新的輸入。但彈簧可以——新的輸入只是改變了目標,運動仍然是連續的。對於任何使用者可以觸碰到的東西,都應該使用彈簧。
Apple 刻意用兩個設計師友善的參數取代了物理三元組(質量/剛度/阻尼)。用以下概念思考:
- Damping ratio (阻尼比) — 控制過衝。
1.0= 臨界阻尼,沒有彈跳,平滑穩定。< 1.0= 會過衝並產生震盪。數值越低越有彈性。 - Response (響應時間) — 值多快能到達目標,單位為秒。數值越低越靈敏。這不是「持續時間」——彈簧沒有固定的持續時間;其穩定時間是由參數共同決定的。
預設值:
- 大多數 UI 從 阻尼
1.0(臨界阻尼)開始——優雅且不分散注意力。 - 僅在手勢本身攜帶了動量時(例如輕彈、拋擲、拖曳後釋放),才加入彈性(阻尼約
0.8)。一個剛淡入的選單有過衝會感覺不對;但你輕彈出去的一張卡片有些微過衝則感覺很自然。
Apple 出貨的具體數值:
| 互動 | 阻尼 | 響應時間 |
|---|---|---|
| 移動/重新定位(例如 PiP) | 1.0 | 0.4 |
| 旋轉 | 0.8 | 0.4 |
| 抽屜 / Sheet | 0.8 | 0.3 |
Web 對應 (Motion / Framer Motion): bounce + duration 的彈簧 API 與 Apple 的阻尼 + 響應時間非常接近。一個安全的風格是預設到處使用 damping: 1.0 的彈簧;將彈性保留給具動量的、物理性的互動。
import { animate } from 'motion';
// 臨界阻尼預設(無過衝)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// 動量互動——稍微彈跳,僅因為前面有輕彈動作
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });
5. 速度交接 — 拖曳與動畫之間的接縫
當一個手勢結束時,動畫必須以手指的準確速度繼續進行,這樣拖曳和動畫之間就不會有可見的接縫。這是區分「流暢」與「還好」的最關鍵細節之一。
將指標釋放時的速度作為彈簧的初始速度傳入。有些彈簧 API 需要相對速度——需根據到目標的剩餘距離進行歸一化:
relativeVelocity = gestureVelocity / (targetValue − currentValue)
例如:元素在 y=50,目標在 y=150(還有 100px 要走),手指速度為 50px/s → 初始彈簧速度 = 50 / 100 = 0.5。Framer Motion / Motion 可以直接接受絕對的 px/s 速度(velocity 選項),所以你通常直接傳入原始值即可。
6. 動量投射 — 動畫應該去到手勢將要抵達的位置
「以小輸入,產生大輸出。」
不要從釋放點直接對齊到最近的邊界。利用速度來投射出最終靜止位置——就像滾動減速一樣——然後對齊到該投射點最近的目標。這就是為什麼一個輕彈會感覺像是把元素「丟」出去。
Apple 的準確投射函數(來自《設計流暢介面》的範例程式碼):
// decelerationRate ≈ 0.998 (正常滾動感); 0.99 (更靈敏)
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // 根據投射結果選擇目標
animateSpringTo(target, { velocity: releaseVelocity }); // 然後進行速度交接 (§5)
注意:物理課本上的 v²/(2·decel) 公式不是 Apple 使用的方式——請使用上面這種指數衰減形式。這是好的底部 Sheet 和輪播庫(Vaul、Embla)的標準行為。
7. 空間一致性 — 對稱路徑、錨定原點
「如果某物以一種方式消失,我們會預期它從它來的地方出現。」
- 進場與退場應遵循同一條路徑。 一個從右邊滑入的面板,應該向右邊關閉。從右邊進來、從下面出去會讓人感覺不連貫且困惑。
- 將互動錨定到其來源。 一個選單、彈出視窗或 Sheet 應該從觸發它的元素位置開始——將
transform-origin設定為觸發器,這樣按鈕與內容之間的空間關係就很明顯。(這與彈出視窗從觸發器縮放而非從中心縮放,是同一個來源感知原則。) - 在可逆轉場上鏡像其緩動效果,使出去的路徑與返回的路徑匹配(對兩個方向使用反相的 cubic-bézier 控制點)。
8. 在手勢方向上給予提示
人類會從軌跡中預測最終狀態。中間的運動應該傳達出事物將往何處去——控制中心的模組會「朝你的手指方向向上向外生長」。讓中間的影格指向結果,而不僅僅是盲目地進行內插。
9. 橡皮筋效果 — 軟邊界
在邊界處,不要硬生生停止,而是逐漸增加阻力。硬停止讀起來像是「凍結」;而連續的阻力則讀起來像是「有回應,但這裡已經沒有東西了」。應用阻尼時,使其隨著使用者拖曳越過邊界的距離而增加。
// 超過邊界越多,元素跟隨得越少——真實的物體在停止前會減速
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}
10. 手勢設計細節(「手感」檢查清單)
- 點擊: 在手指按下時立即高亮(即時),在手指抬起時才確認動作。在目標周圍加入約 10px 的滯後/點擊緩衝區,並允許透過拖曳離開再回來的方式取消。
- 拖曳/滑動: 在決定方向之前,需要一個小的移動門檻(滯後,約 10px),然後再進行 1:1 追蹤。
- 從第一次移動起就並行偵測所有可能的手勢,一旦意圖明確,就自信地取消那些失敗的假設。避免使用那些只報告最終狀態的回應器(例如
swipeleft類型的事件)——它們會拋棄你實現連續回饋所需的持續追蹤。 - 最小化解歧義的延遲。 雙擊偵測無可避免地會延遲單擊;只有在確實需要雙擊功能的地方才付出這個成本。
11. 影格級別的平滑度
平滑度關乎的是影格裡有什麼,而不僅僅是影格率。
- 保持每個影格之間的位置變化低於感知門檻,以避免閃爍。
- 對於非常快的動作,加入細微的動態模糊/拉伸可以編碼速度感,比硬生生的清晰殘影更好讀。
requestAnimationFrame是 Web 上與顯示器同步的時鐘(Apple 使用CADisplayLink)。僅對合成器友善的屬性——transform和opacity——進行動畫,並在動畫即將發生時使用will-change提示。
12. 材質與深度 — 半透明傳達層級
Apple 使用半透明材質作為一個浮動的功能層,它帶來結構感,同時不搶走焦點。在 Web 上,可以使用 backdrop-filter 來近似實現。
- 將導航欄/工具欄/Sheet 建構為半透明層(
backdrop-filter: blur()加上半透明背景),並讓內容在下方滾動——而不是使用佔據固定區域的不透明條。 - 材質的輕重編碼了層級: 較暗/較重的材質區分結構性區域(側邊欄);較輕的材質將注意力吸引到互動元素(按鈕)。切勿將一個淺色半透明表面疊在另一個上面——可讀性會完全崩壞。
- 較大的表面應感覺更厚: 比較小的卡片有更強的模糊效果和更深的陰影。考慮上下文感知的陰影——在內容繁忙/有文字的背景上使用較重的陰影以增加區隔,在單純的背景上使用較輕的陰影。
- 變暗以聚焦,分離以保持流程。 一個模态任務會將表面與一個變暗的遮罩層搭配,並將背景向後/向下推。一個並行的、非阻斷的面板則使用半透明和偏移而不用遮罩層,以免打斷流程。對於疊層的 Sheet,逐漸變暗並將每個父層向後推。
- Vibrancy(鮮明度)讓文字在變化的背景上保持可讀。 在模糊/半透明表面上,不要使用純灰色文字——應使用更高對比度、稍粗的字重,以及微小的字距提升。將色彩放在實色層上,而不是半透明的前景層上。
- 滾動邊緣效果,而不是硬分隔線。 與其在固定標題下方使用 1px 邊框,不如在浮動 UI 實際與內容重疊的地方,淡化一個小的模糊/漸變遮罩。
- 材質化,而不僅僅是淡入淡出。 對於玻璃/模糊表面,在進出場時同時對模糊半徑和縮放進行動畫,使表面讀起來像一個真實的材質到來,而不僅僅是一個單純的不透明度淡入。
.toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* 亮色頂部邊緣 = 光線捕捉到材質 */
}
13. 多模態回饋 — 動畫 + 聲音 + 觸覺
結合感官的三個規則(來自《設計音頻觸覺體驗》):
- 因果關係 — 回饋的原因必須顯而易見。在實際的因果事件發生時觸發它(例如切換開關翻轉、項目就定位),並使其特性與動作的物理性相匹配。
- 和諧 — 視覺、聲音和觸覺必須在同一個影格內觸發。它們之間的延遲會破壞幻覺。不要讓一個音效在動畫結束後才播放。
- 意象 — 使用自然、隱喻性的聲音與紋理(例如,微小的「滴答」聲表示鎖定、柔和的「砰」聲表示就定位、布料摩擦聲表示拖曳)。避免合成或機械化的音效。
14. 減少動畫 — 尊重系統偏好
「不要因為它看起來很酷就讓它動。要因為它服務於某個目的才讓它動。」
- 偵聽
prefers-reduced-motion:在減少動畫模式下,移除所有非必要的縮放、彈跳、平移和持續動畫。保留必要的位置和透明度變化,但將其設定為極短(≤200ms)或無動畫。 - 在減少動畫模式下,彈簧應表現為剛性的、臨界阻尼的(類似於 0.1s linear)。沒有彈跳,沒有蜿蜒。
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}
15. 節制 — 沉默是金
「好的設計是盡可能少的設計。」
- 將設計系統建立在單一一組一致的彈簧參數上——通常僅使用兩或三種「口味」(例如:用於物件移動的
damping: 1.0, response: 0.4,用於縮放的0.3,以及用於動量驅動互動的0.2彈性)。 - 當一個微互動是平凡的或可預測時,不要對其進行動畫——關於「如果它看起來像一般的 UI,就不值得為其加上彈簧」。
- 不同元素之間的轉換減少而非計算其差異:一個元素從列表轉變為詳細檢視時,應無縫變形,而不是一個淡出、一個淡入。這是 Apple 的「大膽過渡」方法。
16. 設計基礎 — 為何這一切都很重要
「我們的目標是創造一種體驗,讓工具的感覺消失,只留下人與內容之間的直接連結。」
Apple 在《設計流暢介面》中將四個核心需求與特定的設計模式相對應。理解這些對應關係有助於在站不住腳的邊緣情況中進行權衡:
- 安全/可預測性 → 空間一致性(第 7 條)、軟邊界(第 9 條)、一致的彈簧行為。使用者應該能夠預測事物的反應。
- 理解 → 即時回饋(第 1 條)、可中斷性(第 3 條)、動量投射(第 6 條)。運動解釋了發生了什麼事以及為什麼。
- 成就感 → 回應速度(第 1 條)、直接操控(第 2 條)、速度交接(第 5 條)。當介面精準地按照我的意圖行動時,感覺很棒。
- 喜悅 → 半透明材質與深度(第 12 條)、恰到好處的動量彈跳(第 4 條)、多模態回饋(第 13 條)。這些是賦予介面個性的最後潤色。