靜態ViewController過渡動畫
首先實現一個present的自定義動畫。
創建PresentTransition,實現UIViewControllerAnimatedTransitioning協議:
import UIKit
class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.75
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
transitionAnimator(using: transitionContext).startAnimation()
}
func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
let to = transitionContext.view(forKey: .to)!
container.addSubview(to)
to.transform = CGAffineTransform(scaleX: 1.33, y: 1.33)
.concatenating(CGAffineTransform(translationX: 0.0, y: 200))
to.alpha = 0
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)
animator.addAnimations({
to.transform = CGAffineTransform(translationX: 0.0, y: 100)
}, delayFactor: 0.15)
animator.addAnimations({
to.alpha = 1.0
}, delayFactor: 0.5)
animator.addCompletion { (_) in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return animator
}
}
製作present動畫看上去很簡單,前面章節已經介紹過了,這裏不做贅述。
將動畫運用於UIViewControllerTransitioningDelegate的代理方法:
extension LockScreenViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return presentTransition
}
}
present到編輯頁面:
@IBAction func presentSettings(_ sender: Any? = nil) {
//present the view controller
settingsController = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController") as! SettingsViewController
settingsController.transitioningDelegate = self
present(settingsController, animated: true, completion: nil)
}
當present到編輯頁面時,因爲編輯頁面背景色是透明的,所以看上去效果是這樣的:
爲PresentTransition添加動畫屬性,讓外部可以注入自己的動畫
var auxAnimations: (() -> Void)?
var auxAnimationsCancel: (()->Void)?
在animator返回前加入:
if let auxAnimations = auxAnimations {
animator.addAnimations(auxAnimations)
}
在LockScreenViewController的presentSettings方法中添加動畫:
presentTransition.auxAnimations = blurAnimations(true)
運行效果:
當用戶點擊cancel時dismiss編輯界面:
settingsController.didDismiss = { [unowned self] in
self.toggleBlur(false)
}
ViewController過渡交互
和UINavigationController動畫交互類似,所使用的類是一樣的,都是UIPercentDrivenInteractiveTransition。
替換PresentTrasition中的類繼承:
class PresentTransition: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning
UIPercentDrivenInteractiveTransition的三個主要方法(update,cancel,finish)前面已經介紹過了。它還有一些其他的屬性及方法:
timingCurve:跟前面章節一樣,提供交互結束後的動畫曲線
wantsInteractiveStart:默認值爲true,如果設置爲false,你將不能進行交互,你可以通過pause()後繼續進行交互。
paused():調用這個方法將暫停非交互性的過渡動畫,並進入交互模式。
添加新的方法:
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
return transitionAnimator(using: transitionContext)
}
這個方法是UIViewControllerAnimatedTransitioning協議裏面的,它允許你提供一箇中斷的動畫。
你的過渡動畫現在有兩種不同的形式:
1.非交互性的動畫,UIKit將回調animateTransition(using:)代理方法用於實現動畫。
2.交互性的動畫,UIKit將回調interruptibleAnimator(using:)代理方法用於實現動畫。
流程圖爲:
切換到LockScreenViewController,添加UIViewControllerTransitioningDelegate的代理方法:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return presentTransition
}
添加屬性:
var isDragging = false
var isPresentingSettings = false
當用戶下拉table時,設置isDragging爲true,當下拉到足夠距離設置isPresentingSettings爲true,實現scroll代理方法:
extension LockScreenViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard isDragging else {
return
}
if !isPresentingSettings && scrollView.contentOffset.y < -30 {
isPresentingSettings = true
presentTransition.wantsInteractiveStart = true
presentSettings()
return
}
}
}
在scrollViewDidScroll(_:)中添加交互代碼:
if isPresentingSettings {
let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
presentTransition.update(progress)
}
當用戶慢慢下拉時,table慢慢變模糊,運行效果:
當完成和取消交互式過渡時,你需要做一些處理:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
if progress > 0.5 {
presentTransition.finish()
}else {
presentTransition.cancel()
}
isPresentingSettings = false
isDragging = false
}
修改PresentTransition中動畫完成時的block:
animator.addCompletion { position in
switch position {
case .end:
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
default:
transitionContext.completeTransition(false)
}
}
只有當動畫在end位置結束時,纔算present完成。其它都是未完成。
在iOS10下反覆拉列表,過渡開始後立馬取消會出現問題,iOS11下已經修復了這個問題:
這跟visual effect view有關,當這種view放在block動畫裏面,當動畫反轉或者取消時似乎不會被移除掉,所以看起來一團糟,所以我們需要手動將其移除,那就要用到我們前面定義的auxAnimationsCancel。
找到animator.addCompletion,添加代碼到default:
self.auxAnimationsCancel?()
當動畫沒有完成時,它將調用,所以我們在LockScreenViewController中將它移除
presentTransition.auxAnimationsCancel = blurAnimations(false)
這就解決啦。
新的問題又出現了,因爲wantsInteractiveStart默認值是true,所以點擊edit時不會調用animateTransition(using:)中的方法,非交互式動畫將不會開始,所以在點擊edit時修改wantsInteractiveStart爲false
self.presentTransition.wantsInteractiveStart = false
現在我們來考慮一種情況,在用戶點擊“Edit”時,動畫期間當用戶再次點擊屏幕,我們需要暫停轉換,這就需要考慮transition在交互式和非交互式之間切換。
切換到PresentTranstion.swift,你不僅需要分別處理交互式模式和非交互式模式,而且還要處理它們之間的切換,添加屬性來保存動畫上下文:
var context: UIViewControllerContextTransitioning?
var animator: UIViewPropertyAnimator?
在transitionAnimator(using:)裏面添加:
self.animator = animator
self.context = transitionContext
動畫完成時設置爲nil
animator.addCompletion { [unowned self] _ in
self.animator = nil
self.context = nil
}
現在你可以添加一個方法來中斷transition
func interruptTransition() {
guard let context = context else { return }
context.pauseInteractiveTransition()//暫停animator
pause()//將transition切換到交互模式
}
爲了允許在非交互模式下能點擊,你需要設置animator響應手勢
animator.isUserInteractionEnabled = true
當進入交互模式後,允許用戶取消或者完成transition,在LockScreenViewController中添加屬性
var touchesStartPointY: CGFloat?
點擊屏幕時中斷transition
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard presentTransition.wantsInteractiveStart == false, presentTransition.animator != nil else {
return
}
touchesStartPointY = touches.first!.location(in: view).y
presentTransition.interruptTransition()
}
當用戶進行拖動時,修改touchesMoved方法:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let startY = touchesStartPointY else { return }
let currentPoint = touches.first!.location(in: view).y
if currentPoint < startY - 40 {
touchesStartPointY = nil
presentTransition.animator?.addCompletion({ (_) in
self.blurView.effect = nil
})
presentTransition.cancel()
}else if currentPoint > startY + 40 {
touchesStartPointY = nil
presentTransition.finish()
}
}
運行效果