[譯] 構建流暢的交互界面

如何在 iOS 上建立天然的交互手勢及動畫

在 WWDC 2018 上,蘋果設計師進行了一次題爲 「設計流暢的交互界面」 的演講,解釋了 iPhone X 手勢交互體系背後的設計理念。前端

蘋果 WWDC18 演講 「設計流暢的交互界面」android

這是我最喜歡的 WWDC 分享 —— 我十分推薦它ios

此次分享提供了一些技術性指導,這對一個設計演講來講是很特殊的,但它只是一些僞代碼,留下了太多的未知。git

演講中一些看起來像 Swift 的代碼。github

若是你想嘗試實現這些想法,你可能會發現想法和實現是有差距的。spring

個人目的就是經過提供每一個主要話題的可行的代碼例子,來減小差距。swift

咱們會建立 8 個界面。 按鈕,彈簧動畫,自定義界面和更多!後端

這是咱們今天會講到的內容概覽:數組

  1. 「設計流暢的交互界面」演講的概要。
  2. 8 個流暢的交互界面,背後的設計理念和構建的代碼。
  3. 設計師和開發者的實際應用

什麼是流暢的交互界面?

一個流暢交互界面也能夠被描述爲「快」,「順滑」,「天然」或是「奇妙」。它是一種光滑的,無摩擦的體驗,讓你只會感受到它是對的。bash

WWDC 演講認爲流暢的交互界面是「你思想的延伸」或是「天然世界的延伸」。當一個界面是按照人們的想法作事,而不是按照機器的想法時,他就是流暢的。

是什麼讓它們流暢?

流暢的交互界面是響應式的,可中斷的,而且是可重定向的。這是一個 iPhone X 滑動返回首頁的手勢案例:

應用在啓動動畫中是能夠被關閉的。

交互界面即時響應用戶的輸入,能夠在任何進程中中止,甚至能夠中途改變更畫方向。

咱們爲何關注流暢的交互界面?

  1. 流暢的交互界面提高了用戶體驗,讓用戶感受每個交互都是快的,輕量和有意義的。
  2. 它們給予用戶一種掌控感,這爲你的應用與品牌創建了信任感。
  3. 它們很難被構建。一個流暢的交互界面是很難被仿造,這是一個有力的競爭優點。

交互界面

這篇文章剩下的部分,我會爲大家展現怎樣來構建 WWDC 演講中提到的 8 個主要的界面。

圖標表明瞭咱們要構建的 8 個交互界面。

交互界面 #1:計算器按鈕

這個按鈕模仿了 iOS 計算器應用中按鈕的表現行爲。

核心功能

  1. 被點擊時立刻高亮。
  2. 即使處於動畫中也能夠被當即點擊。
  3. 用戶能夠在按住手勢結束時或手指脫離按鈕時取消點擊。
  4. 用戶能夠在按住手勢結束時,手指脫離按鈕和手指重回按鈕來確認點擊。

設計理念

咱們但願按鈕感受是即時響應的,讓用戶知道它們是有功能的。 另外,咱們但願操做是能夠被取消的,若是用戶在按下按鈕時決定撤銷操做。這容許用戶更快的作決定,由於他們能夠在考慮的同時進行操做。

WWDC 演講上的幻燈片,展現了手勢是如何與想法同時進行的,以此讓操做更迅速。

關鍵代碼

第一步是建立一個按鈕,繼承自 UIControl,不是繼承自 UIButtonUIButton 也能夠正常工做,但咱們既然要自定義交互,那咱們就不須要它的任何功能了。

CalculatorButton: UIControl {
    public var value: Int = 0 {
        didSet { label.text = 「\(value)」 }
    }
    private lazy var label: UILabel = { ... }()
}
複製代碼

下一步,咱們會使用 UIControlEvents 來爲各類點擊交互事件分配響應的功能。

addTarget(self, action: #selector(touchDown), for: [.touchDown, .touchDragEnter])
addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])
複製代碼

咱們將 touchDowntouchDragEnter 組合到一個單獨的事件,叫作 touchDown,而且咱們將 touchUpInsidetouchDragExittouchCancel 組合一個單獨的事件,叫作 touchUp

(查看 這個文檔 來獲取全部可用的 UIControlEvents 的描述。)

這讓咱們有兩個方法來處理動畫。

private var animator = UIViewPropertyAnimator()
@objc private func touchDown() {
    animator.stopAnimation(true)
    backgroundColor = highlightedColor
}
@objc private func touchUp() {
    animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut, animations: {
        self.backgroundColor = self.normalColor
    })
    animator.startAnimation()
}
複製代碼

touchDown,咱們根據須要取消存在的動畫,而後立刻將顏色設置成高亮顏色(在這裏是淺灰色)。

touchUp,咱們建立了一個新的 animator 而且將動畫啓動。使用 UIViewPropertyAnimator,能夠輕鬆地取消高亮動畫。

(幻燈片筆記:這不是嚴謹的 iOS 計算器應用中按鈕的表現,它容許手勢從別的按鈕移動到這個按鈕來啓動點擊事件。大多數狀況下,我在這裏建立的按鈕就是 iOS 按鈕的預期行爲)

交互界面 #2:彈簧動畫

這個交互展現了彈簧動畫是如何能夠經過指定一個「阻尼」(反彈)和「響應」(速度)來建立的。

核心功能

  1. 使用「對設計友好」的參數。
  2. 對動畫持續時間無概念。
  3. 可輕易中斷。

設計理念

彈簧是一個很好的動畫模型,由於它的速度和天然的外觀表現。一個彈簧動畫能夠及其迅速的開始,用其大多數的時間來慢慢接近最終狀態。 這對建立一個響應式的交互界面來講是完美的。

設計彈簧動畫時的幾個額外的提醒:

  1. 彈簧動畫不須要有彈性。使用數值爲 1 的阻尼會構建一個動畫,它慢慢的向剩下部分靠近,但沒有任何反彈。大多數動畫應該使用值爲 1 的阻尼。
  2. 嘗試着避免考慮時長。理論上,一個彈簧動畫歷來不會徹底靠近其他的部分,若是強加上時長限制,會形成動畫的不天然。相反,要不斷調整阻尼和響應值,直到它感受對。
  3. 可中斷性是很關鍵的。由於彈簧動畫消耗了它們絕大部分的時間來接近最終值,用戶可能會認爲動畫已經完成而且會嘗試再與它交互。

關鍵代碼

在 UIKit 中,咱們能夠用 UIViewPropertyAnimator 和一個 UISpringTimingParameters 對象來構建一個彈簧動畫。不幸的是,它沒有一個只接受「阻尼」和「響應」的初始化構造器。咱們能獲得的最接近的初始化構造器是 UISpringTimingParameters,它須要質量,硬度,阻尼和初始加速度這幾個參數。

UISpringTimingParameters(mass: CGFloat, stiffness: CGFloat, damping: CGFloat, initialVelocity: CGVector)
複製代碼

咱們但願建立一個簡便的初始化構造器,只使用阻尼和響應這兩個參數,而且將它們映射至須要的質量,硬度和阻尼。

使用一點物理知識,咱們能夠導出咱們須要的公示:

彈簧動畫的常量和阻尼係數的解決方案。

有了這個結果,咱們正好可使用咱們想要的參數來建立咱們本身的 UISpringTimingParameters

extension UISpringTimingParameters {
    convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) {
        let stiffness = pow(2 * .pi / response, 2)
        let damp = 4 * .pi * damping / response
        self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
    }
}
複製代碼

這就是咱們如何能夠指定彈簧動畫到全部其餘的交互界面。

彈簧動畫背後的物理學

想深刻研究彈簧動畫?看看 Christian Schnorr 發的這篇極好的文章:Demystifying UIKit Spring Animations

讀了他的文章以後,我最終理解了彈簧動畫。對 Christian 大大的致敬,由於它幫助我理解了這些動畫背後的數學理論,並且教我如何解二階微分方程。

交互界面 #3:手電筒按鈕

又是一個按鈕,但又不一樣的表現形式。它模仿了 iPhone X 鎖屏上的手電筒按鈕。

核心功能

  1. 須要一個使用 3D Touch 的強力手勢。
  2. 對手勢有反彈提示。
  3. 對確認啓動有震動反饋。

設計理念

蘋果但願建立一個按鈕,它能夠輕易地而且快速地被接觸到,可是並不會被不當心觸發。須要強壓來啓動手電筒是一個很棒的選擇,可是缺乏了功能的可見性和反饋性。

爲了解決這個問題,這個按鈕是有彈性的,而且會隨着用戶按壓的力度來變大。除此以外,有兩個單獨的觸覺震動反饋:一個是在達到要求的力度按壓時,另外一個是按壓結束按鈕被觸發時。這些觸覺模擬了物理按鈕的表現形式。

關鍵代碼

爲了衡量按壓按鈕的力度,咱們可使用 touch 事件提供的 UITouch 對象。

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesMoved(touches, with: event)
    guard let touch = touches.first else { return }
    let force = touch.force / touch.maximumPossibleForce
    let scale = 1 + (maxWidth / minWidth - 1) * force
    transform = CGAffineTransform(scaleX: scale, y: scale)
}
複製代碼

咱們基於用戶按壓力度計算了縮放比例,這樣可讓按鈕隨着用戶按壓力度變大。

既然按鈕能夠被按壓但不會啓動,咱們須要持續追蹤按鈕的實時狀態。

enum ForceState {
    case reset, activated, confirmed
}

private let resetForce: CGFloat = 0.4
private let activationForce: CGFloat = 0.5
private let confirmationForce: CGFloat = 0.49
複製代碼

經過將確認壓力設置到稍小於啓動壓力,防止用戶經過快速的超過壓力閾值來頻繁的啓動和取消啓動按鈕。

對於觸覺反饋,咱們可使用 UIKit 的反饋生成器。

private let activationFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)

private let confirmationFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
複製代碼

最後,對於反彈動畫,咱們可使用 UIViewPropertyAnimator 而且配合咱們前面構建的 UISpringTimingParameters 初始化構造器。

let params = UISpringTimingParameters(damping: 0.4, response: 0.2)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
animator.addAnimations {
    self.transform = CGAffineTransform(scaleX: 1, y: 1)
    self.backgroundColor = self.isOn ? self.onColor : self.offColor
}
animator.startAnimation()
複製代碼

交互界面 #4:橡皮筋動畫

橡皮筋動畫發生在視圖抗拒移動時。一個例子就是當滾動視圖滑到最底部時。

核心功能

  1. 交互界面永遠是可響應的,即便當操做是無效的。
  2. 不一樣步的觸摸追蹤,表明了邊界。
  3. 隨着遠離邊界,移動距離變小。

設計理念

橡皮筋動畫是一種很好的方式來溝通無效的操做,它仍然會給用戶一種掌控感。它溫柔的告訴你這是一個邊界,將它們拉回到有效的狀態。

關鍵代碼

幸運的是,橡皮筋動畫實現起來很直接。

offset = pow(offset, 0.7)
複製代碼

經過使用 0 到 1 之間的一個指數,視圖會隨着遠離原始位置,移動愈來愈少。要移動的少就用一個大的指數,移動的多就使用一個小的指數。

再詳細一點,這段代碼通常是在觸摸移動時,在 UIPanGestureRecognizer 回調中實現的。

var offset = touchPoint.y - originalTouchPoint.y  
offset = offset > 0 ? pow(offset, 0.7) : -pow(-offset, 0.7)  
view.transform = CGAffineTransform(translationX: 0, y: offset)
複製代碼

注意:這並非蘋果如何使用像 scroll view 這些元素來實現橡皮筋動畫。我喜歡這個方法,是由於它簡單,但對不一樣的表現,還有不少更復雜的方法。

交互界面 #5:加速停止

爲了看 iPhone X 上的應用切換,用戶須要從屏幕底部向上滑,而且在中途中止。這個交互界面就是爲了建立這個表現形式。

核心功能

  1. 停止是基於手勢加速度來計算的。
  2. 越快的中止致使越快的響應。
  3. 沒有計時器。

設計理念

流暢的交互界面應該是快速的。計時器產生的延遲,即使很短,也會讓界面感到卡頓。

這個交互十分酷,由於它的反應時間是根據用戶手勢運動的。若是他們很快中止,界面會很快響應。若是他們慢慢中止,界面就慢慢響應。

關鍵代碼

爲了衡量加速度,咱們能夠追蹤最新的拖拽手勢的速度值。

private var velocities = [CGFloat]()
private func track(velocity: CGFloat) {
    if velocities.count < numberOfVelocities {
        velocities.append(velocity)
    } else {
        velocities = Array(velocities.dropFirst())
        velocities.append(velocity)
    }
}
複製代碼

這段代碼更新了 velocities 數組,這樣能夠一直持有最新的 7 個速度值,這些能夠被用來計算加速度值。

爲了判斷加速度是否足夠大,咱們能夠計算數組中第一個速度值和目前速度值的差。

if abs(velocity) > 100 || abs(offset) < 50 { return }
let ratio = abs(firstRecordedVelocity - velocity) / abs(firstRecordedVelocity)
if ratio > 0.9 {
    pauseLabel.alpha = 1
    feedbackGenerator.impactOccurred()
    hasPaused = true
}
複製代碼

咱們也要確保手勢移動有一個最小位移和速度。若是手勢已經慢下來超過 90%,咱們會考慮將它中止。

個人實現並不完美。在個人測試裏,它看起來工做的不錯,但還有機會深刻探索加速度的計算方法。

交互界面 #6:獎勵有自我動量的動畫一些反彈效果

一個抽屜動畫,有打開和關閉狀態,他們會根據手勢的速度有一些反彈。

核心功能

  1. 點擊抽屜動畫,沒有反彈。
  2. 輕彈出抽屜,有反彈。
  3. 可交互,可中斷而且可逆。

設計理念

抽屜動畫展現了這個交互界面的理念。當用戶有必定速度的滑動某個視圖,將動畫附帶一些反彈會更使人滿意。這樣交互界面感受像活得,也更有趣。

當抽屜被點擊時,它的動畫是沒有反彈的,這感受起來是對的,由於點擊時沒有任何明確方向動量的。

當設計自定義的交互界面時,要謹記界面對於不一樣的交互是有不一樣的動畫的。

關鍵代碼

爲了簡化點擊與拖拽手勢的邏輯,咱們可使用一個自定義的手勢識別器的子類,在點擊的一瞬間進入 began 狀態。

class InstantPanGestureRecognizer: UIPanGestureRecognizer {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        self.state = .began
    }
}
複製代碼

這可讓用戶在抽屜運動時,點擊抽屜來中止它,這就像點擊一個正在滾動的滾動視圖。爲了處理這些點擊,咱們能夠檢查當手勢中止時,速度是否爲 0 並繼續動畫。

if yVelocity == 0 {
    animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}
複製代碼

爲了處理帶有速度的手勢,咱們首先須要計算它相對於剩下的總距離的速度。

let fractionRemaining = 1 - animator.fractionComplete
let distanceRemaining = fractionRemaining * closedTransform.ty
if distanceRemaining == 0 {
    animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
    break
}
let relativeVelocity = abs(yVelocity) / distanceRemaining
複製代碼

當咱們可使用這個相對速度時,配合計時變量來繼續這個包含一點反彈的動畫。

let timingParameters = UISpringTimingParameters(damping: 0.8, response: 0.3, initialVelocity: CGVector(dx: relativeVelocity, dy: relativeVelocity))

let newDuration = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters).duration

let durationFactor = CGFloat(newDuration / animator.duration)

animator.continueAnimation(withTimingParameters: timingParameters, durationFactor: durationFactor)
複製代碼

這裏咱們建立有一個新的 UIViewPropertyAnimator 來計算動畫須要的時間,這樣咱們能夠在繼續動畫時提供正確的 durationFactor

關於動畫的迴轉,會更復雜,我這裏就不介紹了。若是你想知道的哦更多,我寫了一個關於這部分的完整的教程:構建更好的 iOS APP 動畫

交互動畫 #7: FaceTime PiP

從新創造 iOS FaceTime 應用中的 picture-in-picture(下文中簡稱 Pip)UI。

核心功能

  1. 輕量,輕快的交互
  2. 投影位置是基於 UIScrollView 的減速速率。
  3. 有遵循手勢最初速度的持續動畫。

關鍵代碼

咱們最終的目的是寫一些這樣的代碼。

let params = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)

let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)

animator.addAnimations {
    self.pipView.center = nearestCornerPosition
}

animator.startAnimation()
複製代碼

咱們但願建立一個帶有初始速度的動畫,而且與拖拽手勢的速度相匹配。而且進行 pip 動畫到最近的角落。

首先,咱們須要計算初始速度。

爲了能作到這個,咱們須要計算基於目前速度,目前爲止和目標爲止的相對速度。

let relativeInitialVelocity = CGVector(
    dx: relativeVelocity(forVelocity: velocity.x, from: pipView.center.x, to: nearestCornerPosition.x),
    dy: relativeVelocity(forVelocity: velocity.y, from: pipView.center.y, to: nearestCornerPosition.y)
)

func relativeVelocity(forVelocity velocity: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat {
    guard currentValue - targetValue != 0 else { return 0 }
    return velocity / (targetValue - currentValue)
}
複製代碼

咱們能夠將速度分解爲 x 和 y 兩部分,而且決定它們各自的相對速度。

下一步,咱們爲 PiP 動畫計算各個角落。

爲了讓咱們的交互界面感受天然而且輕量,咱們要基於它如今的移動來投影 PiP 的最終位置。若是 PiP 能夠滑動而且中止,它最終停在哪裏?

let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let velocity = recognizer.velocity(in: view)
let projectedPosition = CGPoint(
    x: pipView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
    y: pipView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
let nearestCornerPosition = nearestCorner(to: projectedPosition)
複製代碼

咱們可使用 UIScrollView 的減速速率來計算剩下的位置。這很重要,由於它與用戶滑動的肌肉記憶相關。若是一個用戶知道一個視圖須要滾動多遠,他們可使用以前的知識直覺地猜想 PiP 到最終目標須要多大力。

這個減速速率也是很寬泛的,讓交互感到輕量——只須要一個小小的推進就能夠送 PiP 飛到屏幕的另外一端。

咱們可使用「設計流暢的交互界面」演講中的投影方法來計算最終的投影位置。

/// Distance traveled after decelerating to zero velocity at a constant rate.
func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
    return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
}
複製代碼

最後缺失的一塊就是基於投影位置找到最近的角落的邏輯。咱們能夠循環全部角落的位置而且找到一個和投影位置距離最小的角落。

func nearestCorner(to point: CGPoint) -> CGPoint {
    var minDistance = CGFloat.greatestFiniteMagnitude
    var closestPosition = CGPoint.zero
    for position in pipPositions {
        let distance = point.distance(to: position)
        if distance < minDistance {
            closestPosition = position
            minDistance = distance
        }
    }
    return closestPosition
}
複製代碼

總結最終的實現:咱們使用了 UIScrollView 的減速速率來投影 pip 的運動到它最終的位置,而且計算了相對速度傳入了 UISpringTimingParameters

交互界面 #8: 旋轉

將 PiP 的原理應用到一個旋轉動畫。

核心功能

  1. 使用投影來遵循手勢的速度。
  2. 永遠停在一個有效的方向。

關鍵代碼

這裏的代碼和前面的 PiP 很像。 咱們會使用一樣的構造回調,除了將 nearestCorner 方法換成 closestAngle

func project(...) { ... }

func relativeVelocity(...) { ... }

func closestAngle(...) { ... }
複製代碼

當最終是時候建立一個 UISpringTimingParameters,針對初始速度,咱們是須要使用一個 CGVector,即便咱們的旋轉只有一個維度。任何狀況下,若是動畫屬性只有一個維度,將 dx 值設爲指望的速度,將 dy 值設爲 0。

let timingParameters = UISpringTimingParameters(  
    damping: 0.8,  
    response: 0.4,  
    initialVelocity: CGVector(dx: relativeInitialVelocity, dy: 0)  
)
複製代碼

在內部,動畫會忽略 dy 的值而使用 dx 的值來建立時間曲線。

本身試一試!

這些交互在真機上更有趣。要本身玩一下這些交互的,這個是 demo 應用,能夠在 GitHub 上獲取到。

流暢的交互界面 demo 應用,能夠在 GitHub 上獲取!

實際應用

對於設計師

  1. 把交互界面考慮成流程的表達中介,而不是一些固定元素的組合。
  2. 在設計流程早期就考慮動畫和手勢。Sketch 這些排版工具是很好用的,可是並不會像設備同樣提供完整的表現。
  3. 跟開發工程師進行原型展現。讓有設計思惟的開發工程師幫助你開發原型的動畫,手勢和觸覺反饋。

對於開發工程師

  1. 將這些建議應用到你本身開發的自定義交互組件上。考慮如何更有趣的將它們結合到一塊兒。
  2. 告訴你的設計師關於這些新的可能。許多設計師沒有意識到 3D touch,觸覺反饋,手勢和彈簧動畫的真正力量。
  3. 與設計師一塊兒演示原型。幫助他們在真機上查看本身的設計,而且建立一些工具幫助他們,來讓設計更加的有效率。

若是你喜歡這篇文章,請留下一些鼓掌。 👏👏👏

你能夠點擊鼓掌 50 次! 因此趕快點啊! 😉

請將這篇文章在社交媒體上分享給你的 iOS 設計師/iOS 開發工程師朋友。

若是你喜歡這種內容,你應該在 Twitter 上關注我。我只發高質量的內容。twitter.com/nathangitte…

感謝 David Okun 校對。

感謝 Christian SchnorrDavid Okun

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索