[译] 构建流畅的交互界面

如何在 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索