《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者需要了解的 iOS 10 新特性,每周更新。本系列翻譯(文集地址)已取得官方授權。目錄點此。倉薯翻譯,歡迎指正:)
Shinobicontrols 為 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤其是圖表方面的控件。 官網 : shinobicontrols.com twitter : @shinobicontrols
曾經的黑暗年代
用基于 block 的 UIView animation 來編寫 view 屬性(frame, transform 等等)變化的動畫非常簡單。只需要短短幾行代碼:
view.alpha = 1
UIView.animate(withDuration: 2) {
containerView.alpha = 0
}
你可以指定動畫結束之后調用的 completion block。如果默認的勻速動畫不能滿足你的要求,還可以調整時間曲線。
但是,如果你需要一種自定義的曲線動畫,相應的屬性變化首先要快速開始,然后再急速慢下來,該怎么辦呢?另外一個有點麻煩的問題是,怎么取消正在進行中的動畫?雖然這些問題都可以解決,用第三方庫或者創建一個新的 animation 來取代進行中的 animation。但蘋果在 UIKit 中新加的組件能把這些步驟簡化許多:進入UIViewPropertyAnimator
的世界吧!
Animation 的新紀元
UIViewPropertyAnimator
的 API 設計得很完善,可擴展性也很好。它 cover 了傳統 UIView animation 動畫的絕大部分功能,并且大大增強了你對動畫過程的掌控能力。具體來說,你可以在動畫過程中任意時刻暫停,可以隨后再選擇繼續,甚至還能在動畫過程中動態改變動畫的屬性(例如,本來動畫終點在屏幕左下角的,可以在動畫過程中把終點改到右上角)。
為了探索這個新的類,我們來看幾個例子,這幾個例子都是演示一張圖片劃過屏幕的動畫。如同所有 Day by Day 系列的文章,例子的代碼可以在 Github 上下載到。這次我們用的是 Playground。
Playground 的準備
我們所有的 playground 頁面都是讓一個小忍者劃過屏幕的動畫。為了方便對比這些頁面的代碼,我們把公共部分的代碼藏在 Sources
文件夾里。這樣不僅能簡化每個頁面的代碼,還能加快編譯過程,因為 Sources
里的代碼是預編譯過的。
Sources
里包含一個簡單的UIView
子類,叫做NinjaContainerView
。它的唯一功能就是添加一個 UIImageView
作為子 view,來顯示我們的小忍者。我把忍者圖片加到了 Resources
里。
import UIKit
public class NinjaContainerView: UIView {
public let ninja: UIImageView = {
let image = UIImage(named: "ninja")
let view = UIImageView(image: image)
view.frame = CGRect(x: 0, y: 0, width: 45, height: 39)
return view
}()
public override init(frame: CGRect) {
// Animating view
super.init(frame: frame)
// Position ninja in the bottom left of the view
ninja.center = {
let x = (frame.minX + ninja.frame.width / 2)
let y = (frame.maxY - ninja.frame.height / 2)
return CGPoint(x: x, y: y)
}()
// Add image to the container
addSubview(ninja)
backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Moves the ninja view to the bottom right of its container, positioned just inside.
public func moveNinjaToBottomRight() {
ninja.center = {
let x = (frame.maxX - ninja.frame.width / 2)
let y = (frame.maxY - ninja.frame.height / 2)
return CGPoint(x: x, y: y)
}()
}
}
現在,在每個 playground 頁面里,我們可以復制粘貼以下代碼:
import UIKit
import PlaygroundSupport
// Container for our animating view
let containerView = NinjaContainerView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
let ninja = containerView.ninja
// Show the container view in the Assistant Editor
PlaygroundPage.current.liveView = containerView
這樣我們就可以用上 Playground 強大的 "Live View" 功能,不用啟動 iOS 模擬器就可以展示動畫效果。盡管 Playground 還是有些不好用的地方,但用來嘗試新功能是非常合適的。
要顯示 Live View,點擊菜單欄上的 View -> Assistant Editor -> Show Assistant Editor,或者點擊右上角工具欄里兩環相套的圖標。如果在右半邊的編輯器里沒有看到 live view,要確保選中的是 Timeline 而不是 Manual —— 不得不承認我在這里浪費了一點時間。
從簡單的開始
UIViewPropertyAnimator
的用法可以跟傳統的 animation block 一樣:
UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
containerView.moveNinjaToBottomRight()
}.startAnimation()
這會觸發一個時長為 1 秒,時間曲線是緩進緩出的動畫。動畫的內容是閉包里的部分。
注意我們是通過調用 startAnimation()
來顯式啟動動畫的。另外一種創建 animator 的方法可以不用手動啟動動畫,就是 runningPropertyAnimator(withDuration:delay:options:animations:completion:)
。確實有點長,所以可能還不如用第一種。
先創建好 animator ,再往上添加動畫也很容易:
// view 設置好之后,我們先來一個簡單的動畫
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut)
// 添加第一個 animation block
animator.addAnimations {
containerView.moveNinjaToBottomRight()
}
// 然后再加第二個
animator.addAnimations {
ninja.alpha = 0
}
這兩個 animation block 會同時進行。
添加 completion block 的方法也很類似:
animator.addCompletion {
_ in
print("Animation completed")
}
animator.addCompletion {
position in
switch position {
case .end: print("Completion handler called at end of animation")
case .current: print("Completion handler called mid-way through animation")
case .start: print("Completion handler called at start of animation")
}
}
如果動畫完整跑完的話,我們可以在控制臺看到以下信息:
Animation completed
Completion handler called at end of animation
進度拖拽和反向動畫
我們可以利用 animator 讓動畫跟隨拖拽的進度進行:
let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn)
// Add our first animation block
animator.addAnimations {
containerView.moveNinjaToBottomRight()
}
let scrubber = UISlider(frame: CGRect(x: 0, y: 0, width: containerView.frame.width, height: 50))
containerView.addSubview(scrubber)
let eventListener = EventListener()
eventListener.eventFired = {
animator.fractionComplete = CGFloat(scrubber.value)
}
scrubber.addTarget(eventListener, action: #selector(EventListener.handleEvent), for: .valueChanged)
Playground 總體來說是很好用的,而且還能在 Live View 里面添加可交互的 UI 控件。然而,接受響應事件就有點麻煩,因為我們需要一個
NSObject
的子類來監聽諸如.valueChanged
這種事件。所以,我們簡單創建一個EventListener
,一旦觸發它的handleEvent
方法,它會調用我們的eventFired
閉包。
這里 fractionComplete
值的計算方法跟時間沒有關系了,所以我們的小忍者不再像之前指定的一樣,會優雅地緩動。
Property animator 最強大的功能體現在它能隨時打斷正在進行的動畫。讓動畫反向也非常容易,只需設置 isReversed
屬性即可。
為了演示這一點,我們使用關鍵幀動畫,這樣就可以制作一個多階段的動畫了:
animator.addAnimations {
UIView.animateKeyframes(withDuration: animationDuration, delay: 0, options: [.calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
ninja.center = containerView.center
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
containerView.moveNinjaToBottomRight()
}
})
}
let button = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 30)))
button.setTitle("Reverse", for: .normal)
button.setTitleColor(.black(), for: .normal)
button.setTitleColor(.gray(), for: .highlighted)
let listener = EventListener()
listener.eventFired = {
animator.isReversed = true
}
button.addTarget(listener, action: #selector(EventListener.handleEvent), for: .touchUpInside)
containerView.addSubview(button)
animator.startAnimation()
按下按鈕的時候,animator 就會把動畫反向進行,只要這一時刻動畫還沒結束。
自定義時間曲線
Property animator 在簡潔優美的同時,還有很強的擴展性。如果你需要在蘋果提供的時間函數之外自定義另一種時間曲線,只需傳進一個實現 UITimingCurveProvider
協議的對象。大部分情況下用到的是 UICubicTimingParameters
或者 UISpringTimingParameters
。
例如,我們想讓小忍者在劃過屏幕的過程中,先快速加速,然后再慢慢停止。如下圖的貝塞爾曲線所示(繪制曲線用了這個很方便的在線工具):
let bezierParams = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.05, y: 0.95),
controlPoint2: CGPoint(x: 0.15, y: 0.95))
let animator = UIViewPropertyAnimator(duration: 4, timingParameters:bezierParams)
animator.addAnimations {
containerView.moveNinjaToBottomRight()
}
animator.startAnimation()
擴展閱讀
新的 property animator 讓編寫動畫更簡單,它的 API 跟傳統方法類似,還添加了打斷動畫、自定義時間曲線等功能。
Apple 為 UIViewPropertyAnimator
提供了詳盡的文檔。另外,也可以看看這場 WWDC 視頻,深度解讀這些新的 API,還講了怎么用新的 API 來做 viewController 跳轉的過渡動畫。另外還有一些有趣的例子,例如一些簡單的游戲。
原作者:Sam Burnstone @sam_burnstone
ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols
譯者:戴倉薯