一步步教你实现类似于格瓦拉启动页中的放大转场动画(Objecti
一、前言
用过格瓦拉电影,或者其他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
版本)