在iOS開發中,界面間的跳轉其實也就是控制器的跳轉,跳轉有很多種,最常用的有push,modal.
- modal:任何控制器都能通過Modal的形式展?出來.效果:新控制器從屏幕的最底部往上鉆,直到蓋住之前的控制器為?.系統也會帶有一個動畫.
public func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
public func dismissViewControllerAnimated(flag: Bool, completion: (() -> Void)?)
- push: 在push中,控制器的管理其實交給了UINavigationController,所以在push控制器的時候必須拿到對應的導航控制器. 效果:從右往左出現,系統會帶有一個默認動畫.
public func pushViewController(viewController: UIViewController, animated: Bool)
public func popViewControllerAnimated(animated: Bool) -> UIViewController?
push和model都有系統提供轉場動畫效果,但有時系統提供的不一定能滿足開發需求,這就需要去自定義,說實話自定義轉場動畫還是有些麻煩的.但原理其實很簡單.
轉場動畫的原理:
當兩個控制器發生push.pop或modal.dismiss的時候,系統會把原始的控制器放到負責轉場的控制器容器中,也會把目標控制器放進去,但是目標控制器是不可見的,因此我們要做的就是把新的控制器顯現出來,把老的控制器移除掉.很簡單吧!
上面大概介紹了一下控制器的切換及原理,這篇文章我們打算說說自定義轉場動畫,一個push一個modal,雖然自定義modal轉場動畫用的比較多一點,但push也可以了解一下嘛!
先來看下效果,然后準備上車:
1.自定義modal轉場動畫
1.簡單的modal效果:
很簡單的modal效果,首先有一個主控制器,主控制器底部有一個scrollView,對scrollView里面圖片添加手勢監聽,并在監聽方法里面modal一個背景圖片一樣的控制器,這樣就可以開始自定義轉場動畫啦!
2.設置轉場動畫的代理:
- 新建一個繼承 NSObject, UIViewControllerAnimatedTransitioning的PopAnimator文件,用于設置轉場動畫的代理方法,在里面添加UIViewControllerAnimatedTransitioning協議必須要實現的兩個代理方法:
// 設置轉場動畫持續時間
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0
}
// 執行轉場動畫
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
}
- 在ViewController(主控制器)中設置herbDetailsVc(每張圖片對應的控制器)的 transitioningDelegate為自己,為剛剛新建的文件設置一個常量 let transition = PopAnimator(),新建一個extension遵守代理UIViewControllerTransitioningDelegate
extension ViewController: UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transition
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
}
如果返回nil,那就使用系統默認的轉場動畫.
3. 創建轉場動畫.
在PopAnimator的transitionDuration:方法中設置好時間.設定轉場動畫的內容
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// 獲得容器
let containerView = transitionContext.containerView()!
// 獲得目標view
// viewForKey 獲取新的和老的控制器的view
// viewControllerForKey 獲取新的和老的控制器
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animateWithDuration(duration, animations: { () -> Void in
toView.alpha = 1.0
}) { (_) -> Void in
// 轉場動畫完成
transitionContext.completeTransition(true)
}
}
內容是:通過修改透明度,達到一個漸變的效果,如下圖所示.
這樣一個簡單的轉場效果就實現啦!是不是很簡單,只是這還不是我們想要的效果.
4.左上角彈出效果
注意:這一步是在第3步的基礎上修改的.在設置PopAnimator中添加 var presenting = true.用于判斷到底是彈出控制器,還是后退,因為前進和后退都會調用animateTransition這個代理方法.下一個實現效果雖然用不到,但我們先這樣去設置.
還要設置一個 var originFrame = CGRect.zero 用于設置目的控制器的frame,將animateTransition中原來的代碼更改為如下
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// 獲得容器
let containerView = transitionContext.containerView()!
// 獲得目標view
// viewForKey 獲取新的和老的控制器的view
// viewControllerForKey 獲取新的和老的控制器
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
// 拿到需要做動畫的view
let herbView = presenting ? toView : fromView
// 獲取初始和最終的frame
let initialFrame = presenting ? originFrame : herbView.frame
let finalFrame = presenting ? herbView.frame : originFrame
// 設置收縮比率
let xScaleFactor = presenting ? initialFrame.width / finalFrame.width : finalFrame.width / initialFrame.width
let yScaleFactor = presenting ? initialFrame.height / finalFrame.height : finalFrame.height / initialFrame.height
let scaleTransform = CGAffineTransformMakeScale(xScaleFactor, yScaleFactor)
// 當presenting的時候,設置herbView的初始位置
if presenting {
herbView.transform = scaleTransform
herbView.center = CGPoint(x: CGRectGetMidX(initialFrame), y: CGRectGetMidY(initialFrame))
herbView.clipsToBounds = true
}
containerView.addSubview(toView)
// 保證在最前,不然添加的東西看不到哦
containerView.bringSubviewToFront(herbView)
// 加了個彈性效果
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, options: [], animations: { () -> Void in
herbView.transform = self.presenting ? CGAffineTransformIdentity : scaleTransform
herbView.center = CGPoint(x: CGRectGetMidX(finalFrame), y: CGRectGetMidY(finalFrame))
}) { (_) -> Void in
transitionContext.completeTransition(true)
}
}
效果如下:
仔細觀察發現無論是presentViewController還是dismissViewController視圖都是從左上角彈出來的.因為我們將originFrame設置為CGRect.zero了.
5. 最終效果實現.
剛剛我們把originFrame設置為了CGRect.zero,但我們可以拿到目標控制器的原始尺寸啊,這樣就不會突兀的從左上角彈出來了.
1.在ViewController的extension里面添加修改如下代碼
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.originFrame = selectedImage!.superview!.convertRect(selectedImage!.frame, toView: nil)
transition.presenting = true
selectedImage!.hidden = true
return transition
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false
return transition
}
2.當dismiss的時候,剛點擊的圖片會消失,因為我們在modal的時候設置為了隱藏,所以在dismiss完成后要將selectedImage顯示出來.可以這樣做,在PopAnimator中添加一個閉包
var dismissCompletion: (()->())?
然后在animateTransition的UIView.animateWithDuration完成閉包中調用
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0.0, options: [], animations: { () -> Void in
herbView.transform = self.presenting ? CGAffineTransformIdentity : scaleTransform
herbView.center = CGPoint(x: CGRectGetMidX(finalFrame), y: CGRectGetMidY(finalFrame))
}) { (_) -> Void in
if !self.presenting {
self.dismissCompletion?()
}
transitionContext.completeTransition(true)
}
最后在ViewController中的viewDidLoad中實現
transition.dismissCompletion = {
self.selectedImage!.hidden = false
}
3.由于scrollview上的圖片有圓角,所以在轉場動畫中我們也要實現圖片圓角與控制器直角的切換.
在PopAnimator中的animateTransition方法中添加如下代碼:
// 設置圓角
let round = CABasicAnimation(keyPath: "cornerRadius")
round.fromValue = !presenting ? 0.0 : 20.0/xScaleFactor
round.toValue = presenting ? 0.0 : 20.0/xScaleFactor
round.duration = duration / 2
herbView.layer.addAnimation(round, forKey: nil)
herbView.layer.cornerRadius = presenting ? 0.0 : 20.0/xScaleFactor
最終自定義modal轉場效果就完成啦!
2. 自定義push轉場動畫
相比自定義modal轉場動畫,自定義push轉場動畫的場景不是很多,原理其實都差不多的.
1. 簡單的push效果.
push是需要導航控制器的,所以在AppDelegate中加載控制器的時候,給它套一個導航控制器.代碼中,ViewController和DetailViewController就是對應切換的兩個控制器.
給ViewController添加一個手勢監聽進行跳轉.
2.老樣子,設置轉場動畫的代理
- 新建一個繼承 NSObject, UIViewControllerAnimatedTransitioning的RevealAnimator文件,用于設置轉場動畫的代理方法,在里面添加UIViewControllerAnimatedTransitioning協議必須要實現的兩個代理方法:
let animationDuration = 2.0
// 用于判斷push或者pop
var operation: UINavigationControllerOperation = .Push
// 設置轉場動畫持續時間
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0
}
// 執行轉場動畫
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
}
- 在ViewController文件中的viewDidLoad方法中拿到導航控制器將它設置為代理
navigationController?.delegate = self
在ViewController文件中添加一個轉場動畫控制器容器屬性
let transition = RevealAnimator()
在ViewController文件中添加一個extension,實現代理方法
extension ViewController : UINavigationControllerDelegate {
/**
- parameter navigationController: 拿到設置代理的導航控制器
- parameter operation: .Push .Pop
- parameter fromVC: 原來的控制器
- parameter toVC: 目標控制器
- returns: 返回設置好的轉場動畫
*/
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.operation = operation
return transition
}
}
3.添加轉場動畫
- 在RevealAnimator中添加一個變量保存animateTransition方法的transitionContext,以后會用到
weak var storedContext: UIViewControllerContextTransitioning?
老樣子在animateTransition添加一些初始化代碼
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! DetailViewController
transitionContext.containerView()?.addSubview(toVC.view)
- 添加LOGO的放大動畫
1.新建一個RWLogoLayer文件,用貝塞爾曲線畫出logo
import UIKit
class RWLogoLayer {
class func logoLayer() -> CAShapeLayer {
let layer = CAShapeLayer()
layer.geometryFlipped = true
let bezier = UIBezierPath()
bezier.moveToPoint(CGPoint(x: 0.0, y: 0.0))
bezier.addCurveToPoint(CGPoint(x: 0.0, y: 66.97), controlPoint1:CGPoint(x: 0.0, y: 0.0), controlPoint2:CGPoint(x: 0.0, y: 57.06))
bezier.addCurveToPoint(CGPoint(x: 16.0, y: 39.0), controlPoint1: CGPoint(x: 27.68, y: 66.97), controlPoint2:CGPoint(x: 42.35, y: 52.75))
bezier.addCurveToPoint(CGPoint(x: 26.0, y: 17.0), controlPoint1: CGPoint(x: 17.35, y: 35.41), controlPoint2:CGPoint(x: 26, y: 17))
bezier.addLineToPoint(CGPoint(x: 38.0, y: 34.0))
bezier.addLineToPoint(CGPoint(x: 49.0, y: 17.0))
bezier.addLineToPoint(CGPoint(x: 67.0, y: 51.27))
bezier.addLineToPoint(CGPoint(x: 67.0, y: 0.0))
bezier.addLineToPoint(CGPoint(x: 0.0, y: 0.0))
bezier.closePath()
layer.path = bezier.CGPath
layer.bounds = CGPathGetBoundingBox(layer.path)
return layer
}
}
- 設置RWLogoLayer的位置尺寸,分別添加到fromVC和toVC上
在ViewController的viewDidAppear中添加
logo.position = CGPoint(x: view.layer.bounds.size.width/2,
y: view.layer.bounds.size.height/2 + 30)
logo.fillColor = UIColor.whiteColor().CGColor
view.layer.addSublayer(logo)
在DetailViewController的viewDidLoad中添加
maskLayer.position = CGPoint(x: view.layer.bounds.size.width/2, y: view.layer.bounds.size.height/2)
view.layer.mask = maskLayer
這邊有個注意點,記得添加
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
view.layer.mask = nil
}
移除mask
- 設置動畫,在RevealAnimator的animateTransition方法中添加
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
// 添加一個陰影效果
animation.toValue = NSValue(CATransform3D:CATransform3DConcat(CATransform3DMakeTranslation(0.0, -10.0, 0.0), CATransform3DMakeScale(150.0, 150.0, 1.0)))
animation.duration = animationDuration
animation.delegate = self
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
// 同時添加到兩個控制器上
toVC.maskLayer.addAnimation(animation, forKey: nil)
fromVC.logo.addAnimation(animation, forKey: nil)
// 給目的控制器設置一個漸變效果
let fadeIn = CABasicAnimation(keyPath: "opacity")
fadeIn.fromValue = 0.0
fadeIn.toValue = 1.0
fadeIn.duration = animationDuration
toVC.view.layer.addAnimation(fadeIn, forKey: nil)
到這里push的轉場效果基本完成了,但會發現還有問題,在自定義modal轉場動畫的時候,當轉場動畫完成后 需要設置transitionContext.completeTransition(true),而這邊也是,這樣做是為了在pop的之前把該清理的都清理掉.so重寫animationDidStop
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let context = storedContext {
context.completeTransition(!context.transitionWasCancelled())
let fromVc = context.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController
fromVc.logo.removeAllAnimations()
}
storedContext = nil
}
- push已經完成啦!現在當我們點擊左上角的start按鈕返回時會發生崩潰,是因為我們沒有在RevealAnimator的animateTransition方法中作判斷,到底是push還是pop.
定義一個屬性判斷是pop還是push,將animateTransition中push相關的代碼放到push判斷語句中去
var operation: UINavigationControllerOperation = .Push
if operation == .Push { // push
}else { // pop
}
- 給pop添加一個縮小的效果
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
transitionContext.containerView()?.insertSubview(toView, belowSubview: fromView)
UIView.animateWithDuration(animationDuration, delay: 0.0, options: .CurveEaseIn, animations: {
fromView.transform = CGAffineTransformMakeScale(0.01, 0.01)
}, completion: {_ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
})
至此,就完成啦!不過離最終效果還有一定距離!
4.根據滑動手勢切換控制器
- 在ViewController底部添加一個label,設置好滑動解鎖這幾個字,這只是提醒,我們的會為整個view添加滑動手勢的.
- 將ViewController的點擊監聽事件改為拖動事件.
let pan = UIPanGestureRecognizer(target: self, action: Selector("didPan:"))
view.addGestureRecognizer(pan)
func didPan(recognizer: UIPanGestureRecognizer) {
}
- 根據滑動的偏移量來調整轉場動畫的進度,也就是說轉場動畫要是可以交互的.之前所有的轉場動畫都是開始后,自動結束,不會隨著人的交互而發生任何改變.所以原來的方法不能滿足需要.
這時需要在ViewController的extension中添加
// 返回一個可以交互的轉場動畫
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if !transition.interactive {
return nil
}
return transition
}
在RevealAnimator中添加一個屬性,用于設置是否可以交互
// 是否需要交互
var interactive = false
4.在ViewController的didPan方法中添加手勢識別,這邊只處理一部分,其余的傳到RevealAnimator中進行
switch recognizer.state {
case .Began:
transition.interactive = true
navigationController?.pushViewController(DetailViewController(), animated: true)
default:
transition.handlePan(recognizer)
}
5.在RevealAnimator的handlePan方法中,根據偏移量計算progress
func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(recognizer.view!.superview!)
var progress: CGFloat = abs(translation.x / 200.0)
progress = min(max(progress, 0.01), 0.99)
switch recognizer.state {
case .Changed:
// 更新當前轉場動畫播放進度
updateInteractiveTransition(progress)
case .Cancelled, .Ended:
if operation == .Push { // push
let transitionLayer = storedContext!.containerView()!.layer
transitionLayer.beginTime = CACurrentMediaTime()
if progress < 0.5 {
completionSpeed = -1.0
cancelInteractiveTransition() // 停止轉場動畫,回到from狀態
} else {
completionSpeed = 1.0
finishInteractiveTransition() // 完成轉場動畫,到to狀態
}
} else { // pop
if progress < 0.5 {
cancelInteractiveTransition()
} else {
finishInteractiveTransition()
}
}
// 使得返回可交互的轉場動畫為nil,重置動畫
interactive = false
default:
break
}
}
至此自定義push的轉場動畫也完成啦!
雖然轉場動畫不難,但從頭到尾這一整套邏輯,還是有點繁瑣的,可能有的同學看的有些蒙圈,這是很正常的,因為知識是網狀的啊,線性的邏輯表述并不能表達清楚網狀知識的每一個連接! so,我在下面給出了源碼,給有興趣推敲的同學.
本文整理自 : iOS.Animations.by.Tutorials.v2.0
源碼 : https://github.com/DarielChen/DemoCode
如有疑問,歡迎留言 :-D