本文是投稿文章,作者:HenryCheng
一、前言
用過格瓦拉電影,或者其他app可能都知道,一種點擊按鈕用放大效果實現轉場的動畫現在很流行,效果大致如下
在iOS中,在同一個導航控制器你可以自定義轉場動畫實現兩個viewController之間的過渡。實際上在iOS7之後,通過實現UIViewControllerAnimatedTransitioning或者UIViewControllerContextTransitioning協議,就可以簡單的自定義轉場動畫,比如一個NavigationController的push和pop。還有一點你需要知道的是,我如果有一個矩形,有一個圓,想要在這個矩形上剪出和圓大小相同的面積,那麼就要用到CALayer的mask屬性,下面用圖表達可能會直觀些:
laye.mask
現在可能你對mask屬性有一點了解了,下面代碼的實現中你將會看到具體的實現過程。先做這麼多了解,下面開始一步步實現效果。
二、開始實現簡單的push效果
新建工程,這裡用的是Swift,選中storyboard,然後加上一個導航,如下
添加導航控制器
然後效果如下
去掉導航欄
把右側的Shows Navigation Bar去掉,因為這個demo裡面並不需要導航欄,同時保證Is Instal View Controller是被勾上的(不知道的童鞋可以去掉看一下效果),這裡默認的都是勾選上的。然後在新建一個viewController,並設置其繼承於ViewController,如下
新建一個viewController
然後在兩個VC上分別在同樣的位置添加兩個完全相同的按鈕,位置約束在右上角距離右邊和上邊分別為20,20的距離,為了區分,將這兩個VC設置不同的背景色,如下
按鈕的約束位置以及大小
添加按鈕以及背景色以後效果
然後右鍵一直按住第一個按鈕拖拽至第二個VC(也就是黃色背景的)點擊show
實現第一個 VC 按鈕點擊方法
這時候兩個VC之間就會出現一條線,然後點擊線中間,設置identifier為PushSegue,這裡設置一個標識符,為後面的跳轉做准備,效果如下:
設置identifier
將兩個按鈕連接成ViewController的同一個屬性,名為popBtn,然後將第二個VC的按鈕實現一個點擊方法(因為我們要pop回來)名為popClick,如下
import UIKit class ViewController: UIViewController { @IBOutlet weak var popBtn: UIButton! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } @IBAction func popClick(sender: AnyObject) { self.navigationController?.popViewControllerAnimated(true) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
最後,分別在兩個VC的中間添加一個imageView,最後的效果圖如下
最後效果圖
如果到這裡你還沒錯的話,那麼運行一下你的工程,運行的效果將會是這樣
最後的運行效果圖
沒錯,也就是一個簡單的push效果,現在准備工作已經做好了,想要實現放大效果的動畫,還要繼續往下進行。
三、開始實現放大效果
通過上面的步驟,我們已經做好了准備工作,我們還要知道的一點是,要想自定義導航的push或pop效果,需要實現UINavigationControllerDelegate協議裡面的
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return nil }
這個協議方法,我們先新建一個繼承於NSObject的名為HWNavigationDelegate的一個類,然後引入UINavigationControllerDelegate,實現上面的協議方法,使返回值暫時為nil(從上面代碼中可以看出返回值是一個可選值,所以這裡可以先用nil,待會再具體實現)。然後你的HWNavigationDelegate裡面的代碼大致如下
// // HWNavigationDelegate.swift // HWAnimationTransition_Swift // // Created by HenryCheng on 16/3/16. // Copyright ? 2016年 www.igancao.com. All rights reserved. // import UIKit class HWNavigationDelegate: NSObject, UINavigationControllerDelegate { func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil; } }
現在繼續打開storyboard,然後在右下角搜索Object,並將其拖拽至左邊Navigation Controller Source裡,
添加Object
並在選中Object,在右邊將其類改成剛剛創建的HWNavigationDelegate
HWNavigationDelegate.png
最後在左側,點擊UINavigationController,並將其delegate設置為剛才的Object
設置導航的delegate
現在上面HWNavigationDelegate裡面導航的協議方法的返回值還是nil,我們需要創建一個實現動畫效果的類,並使其返回,這裡我們新建一個同樣繼承於NSObject的名為HWTransitionAnimator的類,並使其實現UIViewControllerAnimatedTransitioning協議,和其中的協議方法,為了便於閱讀,這裡貼出所有的代碼,
// // HWTransitionAnimator.swift // HWAnimationTransition_Swift // // Created by HenryCheng on 16/3/16. // Copyright ? 2016年 www.igancao.com. All rights reserved. // import UIKit class HWTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { weak var transitionContext: UIViewControllerContextTransitioning? func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { return 0.5 } func animateTransition(transitionContext: UIViewControllerContextTransitioning) { self.transitionContext = transitionContext let containerView = transitionContext.containerView() let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! ViewController let button = fromVC.popBtn containerView?.addSubview(toVC.view) let circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame) let extremePoint = CGPoint(x: button.center.x - 0, y: button.center.y - CGRectGetHeight(toVC.view.bounds)) let radius = sqrt((extremePoint.x * extremePoint.x) + (extremePoint.y * extremePoint.y)) let circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius)) let maskLayer = CAShapeLayer() maskLayer.path = circleMaskPathFinal.CGPath toVC.view.layer.mask = maskLayer let maskLayerAnimation = CABasicAnimation(keyPath: "path") maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath maskLayerAnimation.toValue = circleMaskPathFinal.CGPath maskLayerAnimation.duration = self.transitionDuration(transitionContext) maskLayerAnimation.delegate = self maskLayer.addAnimation(maskLayerAnimation, forKey: "path") } override func animationDidStop(anim: CAAnimation, finished flag: Bool) { self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled()) self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil } }
關於上面的所有代碼,其中func animateTransition(transitionContext: UIViewControllerContextTransitioning),func animateTransition(transitionContext: UIViewControllerContextTransitioning)分別是設置時間和動畫過程的方法,都是UIViewControllerAnimatedTransitioning的協議方法,func animationDidStop是實現動畫結束後的操作,這裡動畫結束後需要做取消動畫和將fromViewController釋放掉的操作。裡面的
let circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame) let extremePoint = CGPoint(x: button.center.x - 0, y: button.center.y - CGRectGetHeight(toVC.view.bounds)) let radius = sqrt((extremePoint.x * extremePoint.x) + (extremePoint.y * extremePoint.y)) let circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius)) let maskLayer = CAShapeLayer() maskLayer.path = circleMaskPathFinal.CGPath toVC.view.layer.mask = maskLayer
這段代碼,下面第二段代碼的maskLayer這個上面開始的時候就說過了,第一段代碼其實就是一個計算的過程,求出最後大圓效果的半徑,原理如圖(粗糙的畫了一下,畫得不好見諒^_^)
動畫效果關鍵的實現原理圖
最後將剛才HWNavigationDelegate裡的協議方法返回值修改成HWTransitionAnimator的對象就可以了
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return HWTransitionAnimator() }
如果上面步驟,你操作沒錯的話,運行工程效果如下
tap_swift
四、添加手勢引導動畫
添加手勢實現動畫效果,我們在剛才的HWNavigationDelegate類裡實現UINavigationControllerDelegate的另外一個斜一方法
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.interactionController }
這裡的self.interactionController就是我們的導航控制器,如下圖
設置導航屬性
然後重寫awakeFromNib()方法,關於整個HWNavigationDelegate最後的代碼實現,如下
// // HWNavigationDelegate.swift // HWAnimationTransition_Swift // // Created by HenryCheng on 16/3/16. // Copyright ? 2016年 www.igancao.com. All rights reserved. // import UIKit class HWNavigationDelegate: NSObject, UINavigationControllerDelegate { @IBOutlet weak var navigationController: UINavigationController! var interactionController: UIPercentDrivenInteractiveTransition? func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.interactionController } func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return HWTransitionAnimator() // return nil; } override func awakeFromNib() { super.awakeFromNib() let panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:")) self.navigationController.view.addGestureRecognizer(panGesture) } func panned(gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { case .Began: self.interactionController = UIPercentDrivenInteractiveTransition() if self.navigationController?.viewControllers.count > 1 { self.navigationController?.popViewControllerAnimated(true) } else { self.navigationController?.topViewController!.performSegueWithIdentifier("PushSegue", sender: nil) } case .Changed: let translation = gestureRecognizer.translationInView(self.navigationController!.view) let completionProgress = translation.x / CGRectGetWidth(self.navigationController!.view.bounds) self.interactionController?.updateInteractiveTransition(completionProgress) case .Ended: if (gestureRecognizer.velocityInView(self.navigationController!.view).x > 0) { self.interactionController?.finishInteractiveTransition() } else { self.interactionController?.cancelInteractiveTransition() } self.interactionController = nil default: self.interactionController?.cancelInteractiveTransition() self.interactionController = nil } } }
這裡需要注意的是gestureRecognizer的幾個狀態
Begin :手勢被識別時時,初始化UIPercentDrivenInteractiveTransition實例對象和設置屬性,比如如果是第一個VC就實現push,反之則是pop
Changed:開始手勢到結束手勢的一個過程,上面代碼中是根據偏移量改變self.interactionController的位置
Ended:手勢結束以後的操作,設置動畫結束或者取消動畫,最後將self.interactionController置為nil
default:其他的狀態運行你的工程,拖拽屏幕時效果如下
pan_swift.gif
五、最後
由於最近工作比較忙,好久沒有寫博客了,趁著這回功夫將這個小動畫分享一下,希望大家喜歡,時間不早了,該回去休息了(在公司加班完成的,喜歡的就star一下吧),最後,這裡只是swift版本的代碼,同時如果你需要全部代碼的話,你可以在下面下載
HWAnimationTransition_Swift(swift版本)
HWAnimationTransition_OC (OC版本)