静态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()
}
}
运行效果