在iOS 10中帶入了一種“全新的,面向對象的,完全可交互和停止的動畫。你可以掌控你的動畫并且和手勢操作相關聯(lián)”。
簡單點說,就是在iOS 10下開發(fā)者可以更加容易的取消、反轉、暫停、重啟動畫,并且可以修改動畫的時序和執(zhí)行時間。而且以上功能在view controller轉場動畫上也可以使用。
開始項目
首先,準備好我們要做動畫的視圖。創(chuàng)建一個新的項目,只是用默認的配置即可。
import UIKit
class ViewController: UIViewController {
var circleCenter: CGPoint!
override func viewDidLoad() {
super.viewDidLoad()
let circle = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
circle.center = self.view.center
circle.layer.cornerRadius = 50.0
circle.backgroundColor = UIColor.green
circle.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.dragCircle)))
self.view.addSubview(circle)
}
func dragCircle(gesture: UIPanGestureRecognizer) {
let target = gesture.view!
switch gesture.state {
case .began, .ended:
circleCenter = target.center
case .changed:
let translation = gesture.translation(in: self.view)
target.center = CGPoint(x: circleCenter.x + translation.x,
y: circleCenter.y + translation.y)
default:break
}
}
沒什么復雜的。只是在viewDidLoad方法里創(chuàng)建了一個綠色的圓形,并放在了屏幕的中央。然后在上面附加了一個UIPanGestureRecognizer實例。這樣圓形就可以相應用戶的拖拽手勢了。響應手勢的方法是dragCircle。
關于UIViewPropertyAnimator
UIViewPropertyAnimator是實現(xiàn)view屬性動畫主要用到的類。UIViewPropertyAnimator實例維護著一組動畫,并且新的動畫基本上任何時候都再添加進去。注意:以后凡是說到這個類的實例就叫做animator。
如果有兩個以上的動畫修改view的同一個屬性,那么最后一個添加或者開始的起作用。最有意思的是,animator添加了新動畫之后不是一下轉到新動畫上,而是有一個過渡的過程。
可中斷,反轉的動畫
現(xiàn)在我們就來開發(fā)一個動畫。用戶拖動的時候圓形逐漸變大為原來size的兩倍,松開之后復原。
首先給view controller添加兩個屬性:
class ViewController: UIViewController {
var circleCenter: CGPoint!
var circleAnimator: UIViewPropertyAnimator!
let animationDuration = 4.0
在viewDidLoad方法里初始化animator:
circleAnimator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeOut, animations: {[unowned circle] in
// 2x scale
circle.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
})
我們在初始化circleAniamtor的時候,傳入了兩個參數(shù)duration和curve。curve可以參數(shù)可以選擇系統(tǒng)預定義的四種類型之一,我們這里選擇了.easeInOut。其他的選項是easeIn、easeOut、和linear。最后出入的參數(shù)是一個描述動畫的閉包。
現(xiàn)在,我們需要在拖動方法里觸發(fā)動畫。
func dragCircle(gesture: UIPanGestureRecognizer) {
let target = gesture.view!
switch gesture.state {
case .began, .ended:
circleCenter = target.center
if(circleAnimator.isRunning) {
circleAnimator.pauseAnimation()
circleAnimator.isReversed = gesture.state == .ended
}
circleAnimator.startAnimation()
case .changed:
let translation = gesture.translation(in: self.view)
target.center = CGPoint(x: circleCenter.x + translation.x,
y: circleCenter.y + translation.y)
default:break
}
}
運行代碼之后,動畫在拖動的時候可以運行了。
但是,一次動畫執(zhí)行完成之后再次拖動的時候APP就會崩潰了。這是為什么呢?
動畫在執(zhí)行依次,animator的引用就會被廢棄。Animator有一下的三個狀態(tài):
- inactive,初始狀態(tài),動畫執(zhí)行完成之后也會回到這個狀態(tài)。
- active,動畫執(zhí)行的時候的狀態(tài)。
- stopped,調(diào)用
stopAnimation方法之后進入這個狀態(tài)。
三個狀態(tài)的轉化見下圖:

只要動畫進入了Inactive,animator的動畫就會被廢棄,包括animator的completion閉包。
我們已經(jīng)見過了startAnimation方法,現(xiàn)在來看看其他的幾個改變動畫狀態(tài)的方法。
首先修改circleAnimator的初始化方法:
circleAnimator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeOut)
修改dragCircle方法:
if circleAnimator.state == .active {
// set to inactive state
circleAnimator.stopAnimation(true)
}
if gesture.state == .began {
circleAnimator.addAnimations {
target.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
}
} else {
circleAnimator.addAnimations {
target.transform = CGAffineTransform.identity
}
}
// ...
現(xiàn)在,無論用戶合適停止拖拽,動畫清空都不會影響下一次的動畫執(zhí)行。因為在下次拖拽的時候會重新添加動畫。
注意:stopAnimation(true)等于:
circleAnimator.stopAnimation(false)
circleAnimator.finishAnimation(at: .current)
stopAnimation(false)不結束,動畫保持在“停滯狀態(tài)”。finishAnimation(at: .current)把view的屬性設置為動畫當前的屬性值。設置view的屬性的值來自UIViewAnimatingPosition的值。如果傳入的是.start或者.end那么圓形會立刻變成動畫開始時或者結束時的樣子。
動畫時間
這個動畫在執(zhí)行的時候還有一個問題。那就是不管圓形的動畫值停留在了哪個值上,下次拖拽的時候動畫還是要執(zhí)行4秒鐘的事件。顯然,有保留值的時候只需要執(zhí)行剩下的值所需要的時間。我們來改進一下:
case .began, .ended:
circleCenter = target.center
let durationFactor = circleAnimator.fractionComplete
circleAnimator.stopAnimation(false)
circleAnimator.finishAnimation(at: .current)
if gesture.state == .began {
circleAnimator.addAnimations {
target.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
}
} else {
circleAnimator.addAnimations {
target.transform = CGAffineTransform.identity
}
}
circleAnimator.startAnimation()
circleAnimator.pauseAnimation()
circleAnimator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor)
case .changed:
// ...
現(xiàn)在我們顯示的停止動畫,根據(jù)方向添加動畫,最后重啟animator。使用circleAnimator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor)方法調(diào)整剩余的時間。這個方法也可以用來調(diào)整animator的動畫時間方法。如果給參數(shù)withTimingParameters傳入一個新的時間方法,動畫的計時方式會平滑的從老的方式過渡到新的方式。
最后
UIViewAnimationCurve是上面我們用的計時方法。但是還有兩個新的計時對象UISpringTimingParameters和UICubicTimingParameters。使用者兩個計時方法可以開發(fā)出更加酷炫的動畫來,稍后我們專門介紹。