iOS開發(fā)文集iOS 10 Day By Day:UIViewPropertyAnimator屬性
基于UIView的動(dòng)畫block在動(dòng)畫屬性(框架、變換,等等)之間添加過渡,輕松建立。他們創(chuàng)建起來是難以置信的容易,只需要幾行代碼:
view.alpha = 1 UIView.animate(withDuration: 2) { containerView.alpha = 0 }
你還可以添加completion blocks,當(dāng)動(dòng)畫完成時(shí)它們將被執(zhí)行,如果默認(rèn)的線性運(yùn)動(dòng)不能運(yùn)行則調(diào)整動(dòng)畫曲線。
然而,如果你需要?jiǎng)?chuàng)建你自己的自定義動(dòng)畫曲線,就需要被動(dòng)畫迅速啟動(dòng)的屬性,然后迅速減速,這時(shí)會(huì)發(fā)生什么呢?另一個(gè)稍微棘手的問題是你需要取消一個(gè)運(yùn)行中的動(dòng)畫。當(dāng)然這些可以通過使用第三方庫或創(chuàng)建一個(gè)新的動(dòng)畫更換目前正在進(jìn)行的動(dòng)畫來解決,蘋果增加了一個(gè)新的組件到UIKit框架,使得這個(gè)問題解決起來變得容易許多:進(jìn)入U(xiǎn)IViewPropertyAnimator。
一個(gè)新的動(dòng)畫時(shí)代
UIViewPropertyAnimator是寫得很好的且高度可擴(kuò)展的API。它涵蓋了許多與“舊式”UIView動(dòng)畫相同的功能,但能給你動(dòng)畫的精確編程控制。這意味著你可以暫停一個(gè)進(jìn)度,在稍后的某個(gè)日期如果你愿意的話再啟動(dòng)它,甚至可以動(dòng)態(tài)修改動(dòng)畫屬性(比如將以前左下角的動(dòng)畫結(jié)束點(diǎn)位置改變到屏幕的右上方)。
為了探索這個(gè)新的類,我們將看幾個(gè)我們?cè)谄聊簧现谱鞯膱D像動(dòng)畫的例子。像所有的Day by Day博客一樣,代碼放在GitHub上(為了方便各位讀者,小編已經(jīng)為大家整理了,請(qǐng)點(diǎn)擊這里下載)。這一次,我們要使用一個(gè)Playground。
Playground演練
我們所有的Playground頁面都有一個(gè)忍者在屏幕上移動(dòng)。為了使頁面盡可能的貼切,我們將我們共同的代碼隱藏在Sources文件夾里。這不僅是幫助你簡化你的頁面的代碼,也使他們的運(yùn)行速度同源被編譯了一樣快。
源包含一個(gè)簡單的UIView子類,叫做NinjaContainerView。這個(gè)通過用顯示我們的忍者的UIImageView子視圖簡單設(shè)置了視圖。我已將圖像添加到Playground的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) }() } }
現(xiàn)在,在每個(gè)Playground頁面,我們可以復(fù)制和粘貼以下代碼:
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”功能,無需啟動(dòng)模擬器就可以給我們一個(gè)我們的動(dòng)畫的可視化演示。他們?nèi)匀挥凶约旱姆绞剑玃laygrounds是嘗試新東西的明智之舉。
要顯示Live View窗格,導(dǎo)航到View > Assistant Editor > Show Assistant Editor,或者,選擇在工具欄中右上方看起來像兩個(gè)相交圓的圖標(biāo)。如果你在Assistant Editor中沒有看到Live View,要確保已選擇了Timeline,而不是Manual,比起我認(rèn)識(shí)到這一點(diǎn),我已經(jīng)失去了更多的時(shí)間!
從簡單的開始
我們可以用同舊式的基于block的API完全相同的方式使用UIViewPropertyAnimator :
UIViewPropertyAnimator(duration: 1, curve: .easeInOut) { containerView.moveNinjaToBottomRight() }.startAnimation()
這將啟動(dòng)一個(gè)持續(xù)1秒的動(dòng)畫,有一個(gè)簡化后的時(shí)間曲線。執(zhí)行的動(dòng)畫在關(guān)閉中。

請(qǐng)注意,我們必須通過調(diào)用startAnimation()明確啟動(dòng)動(dòng)畫。另一種方法來創(chuàng)建無需自身啟動(dòng)的動(dòng)畫是使用runningPropertyAnimator(withDuration:delay:options:animations:completion:)——一個(gè)公平的mouthful,所以你可能更喜歡使用手動(dòng)啟動(dòng)版。
在動(dòng)畫已經(jīng)創(chuàng)建之后,添加額外的動(dòng)畫是很容易的。
// Now we've set up our view, let's animate it with a simple animation let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) // Add our first animation block animator.addAnimations { containerView.moveNinjaToBottomRight() } // Now here goes our second animator.addAnimations { ninja.alpha = 0 }
這些動(dòng)畫blocks會(huì)跑在一起。

Completion blocks可以以類似的方式添加:
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") } }
在動(dòng)畫被允許運(yùn)行它的整個(gè)持續(xù)時(shí)間的情況下,我們將看到以下版塊:
Animation completed Completion handler called at end of animation
滑動(dòng)和反向
我們可以使用animator來滑動(dòng)我們的動(dòng)畫:
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是很棒的,你甚至可以添加交互式UI元素到Live View中。不幸的是,接收事件是痛苦的,因?yàn)槲覀冃枰粋€(gè)符合NSObject可以監(jiān)聽事件的類,像是.valueChanged。為此,當(dāng)handleEvent方法被調(diào)用時(shí),我們用一個(gè)簡單的對(duì)象EventHandler調(diào)用我們的eventFired關(guān)閉。
分?jǐn)?shù)與時(shí)間無關(guān),所以我們看不到我們的忍者在一個(gè)像我們定義的優(yōu)雅的easeIn曲線上移動(dòng)。
屬性動(dòng)畫的真正力量來自于能夠中斷正在進(jìn)行的動(dòng)畫的能力。我們可以通過簡單的切換isReversed屬性來反向移動(dòng)動(dòng)畫。
為了說明這一點(diǎn),我們使用關(guān)鍵幀動(dòng)畫,這樣我們就可以定義一個(gè)多級(jí)動(dòng)畫:
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()
當(dāng)按下按鈕,屬性動(dòng)畫將反轉(zhuǎn)動(dòng)畫,如果其狀態(tài)是激活的(即動(dòng)畫目前在運(yùn)行中,忍者尚未到達(dá)其最終目的地)。

添加你自己的時(shí)間曲線
屬性動(dòng)畫是很簡單的,其余是漂亮的擴(kuò)展。如果你需要另一種蘋果所提供的動(dòng)畫曲線,你可以通過你自己符合UITimingCurveProvider協(xié)議的情況。在大多數(shù)情況下,你可能會(huì)使用UICubicTimingParameters或UISpringTimingParameters。
我們說我們的忍者加速真的很快,然后在屏幕上他們的旅途中出現(xiàn)一個(gè)漸進(jìn)的停止。我們將使用Bezier曲線,如下圖(使用這個(gè)方便的在線工具繪制)。

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()

進(jìn)一步的閱讀
新的屬性動(dòng)畫通過帶來在一致的API下的現(xiàn)有能力,看起來會(huì)讓動(dòng)畫項(xiàng)目比以往任何時(shí)候都容易,增加了中斷動(dòng)畫和你自己的自定義時(shí)序曲線的能力。
蘋果提供了一些優(yōu)秀的UIViewPropertyAnimator文件。或者,你可以觀看WWDC演講,將在新的API進(jìn)行深入研究和探討可以用來如何創(chuàng)建UIViewControllers之間的自定義轉(zhuǎn)換。其他有趣的演示也包括了簡單的游戲。
本文翻譯自: