《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 跳轉的過渡動畫。另外還有一些有趣的例子,例如一些簡單的遊戲。
原文地址:iOS 10 Day by Day :: Day 4 :: UIViewPropertyAnimator
原作者:Sam Burnstone @sam_burnstone
ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols
譯者:戴倉薯