一步步教你实现类似于格瓦拉启动页中的放大转场动画(Objective C & Swift)
一、前言
用过格瓦拉电影,或者其他app
可能都知道,一种点击按钮用放大效果实现转场的动画现在很流行,效果大致如下
在iOS
中,在同一个导航控制器你可以自定义转场动画实现两个viewController
之间的过渡。实际上在iOS7
之后,通过实现UIViewControllerAnimatedTransitioning
或者UIViewControllerContextTransitioning
协议,就可以简单的自定义转场动画,比如一个NavigationController
的push
和pop
。还有一点你需要知道的是,我如果有一个矩形,有一个圆,想要在这个矩形上剪出和圆大小相同的面积,那么就要用到CALayer
的mask
属性,下面用图表达可能会直观些:
现在可能你对mask
属性有一点了解了,下面代码的实现中你将会看到具体的实现过程。先做这么多了解,下面开始一步步实现效果。
二、开始实现简单的push
效果
新建工程,这里用的是Swift
,选中storyboard
,然后加上一个导航,如下
把右侧的Shows Navigation Bar
去掉,因为这个demo
里面并不需要导航栏,同时保证Is Initial View Controller
是被勾上的(不知道的童鞋可以去掉看一下效果),这里默认的都是勾选上的。然后在新建一个viewController
,并设置其继承于viewController
,如下
然后在两个VC
上分别在同样的位置添加两个完全相同的按钮,位置约束在右上角距离右边和上边分别为20
,20
的距离,为了区分,将这两个VC
设置不同的背景色,如下
VC
(也就是黄色背景的)点击show
这时候两个VC
之间就会出现一条线,然后点击线中间,设置identifier
为PushSegue
,这里设置一个标识符,为后面的跳转做准备,效果如下:
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
,在右边将其类改成刚刚创建的HWNavigationDelegate
最后在左侧,点击UINavigationController
,并将其delegate
设置为刚才的Object
现在上面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 transitionDuration(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() }
如果上面步骤,你操作没错的话,运行工程效果如下
四、添加手势引导动画
添加手势实现动画效果,我们在刚才的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
的几个状态
- 1、
Begin
:手势被识别时时,初始化UIPercentDrivenInteractiveTransition
实例对象和设置属性,比如如果是第一个VC
就实现push
,反之则是pop
- 2、
Changed
:开始手势到结束手势的一个过程,上面代码中是根据偏移量改变self.interactionController
的位置 - 3、
Ended
:手势结束以后的操作,设置动画结束或者取消动画,最后将self.interactionController
置为nil
- 4、
default
:其他的状态运行你的工程,拖拽屏幕时效果如下
五、最后
由于最近工作比较忙,好久没有写博客了,趁着这回功夫将这个小动画分享一下,希望大家喜欢,时间不早了,该回去休息了(在公司加班完成的,喜欢的就star
一下吧),最后,这里只是Swift
版本的代码,同时如果你需要全部代码的话,你可以在下面下载
- 1、HWAnimationTransition_Swift(
Swift
版本) - 2、HWAnimationTransition_OC (
OC
版本)