在iOS7之前,开发者为了寻求自定义Navigation Controller的Push/Pop动画,只能受限于子类化一个UINavigationController,或是用自定义的动画去覆盖它。但是随着iOS7的到来,Apple针对开发者推出了新的工具,以更灵活地方式管理UIViewController切换。
效果预览:
自定义导航栏的Push/Pop动画
为了在基于UINavigationController下做自定义的动画切换,先建立一个简单的工程,这个工程的rootViewController是一个UINavigationController,UINavigationController的rootViewController是一个简单的UIViewController(称之为主页面),通过这个UIViewController上的一个Button能进入到下一个UIViewController中(称之为详情页面),我们先在主页面的ViewController上实现两个协议:UINavigationControllerDelegate和UIViewControllerAnimatedTransitioning,然后在ViewDidLoad里面把navigationController的delegate设为self,这样在导航栏Push和Pop的时候我们就知道了,然后用一个属性记下是Push还是Pop,就像这样:
- func navigationController(navigationController: UINavigationController!, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController!, toViewController toVC: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
- navigationOperation = operation
- return self
- }
UIViewControllerAnimatedTransitioning是苹果新增加的一个协议,其目的是在需要使用自定义动画的同时,又不影响视图的其他属性,让你把焦点集中在动画实现的本身上,然后通过在这个协议的回调里编写自定义的动画代码,即“切换中应该会发生什么”,负责切换的具体内容,任何实现了这一协议的对象被称之为动画控制器。你可以借助协议能被任何对象实现的这一特性,从而把各种动画效果封装到不同的类中,只要方便使用和管理,你可以发挥一切手段。我在这里让主页面实现动画控制器也是可以的,因为它是导航栏的rootViewController,会一直存在,我只要在里面编写自定义的Push和Pop动画代码就可以了:
- //UIViewControllerTransitioningDelegate
- func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {
- return 0.4
- }
- func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {
- let containerView = transitionContext.containerView()
- let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
- let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
- var destView: UIView!
- var destTransform: CGAffineTransform!
- if navigationOperation == UINavigationControllerOperation.Push {
- containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)
- destView = toViewController.view
- destView.transform = CGAffineTransformMakeScale(0.1, 0.1)
- destTransform = CGAffineTransformMakeScale(1, 1)
- } else if navigationOperation == UINavigationControllerOperation.Pop {
- containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
- destView = fromViewController.view
- // 如果IDE是Xcode6 Beta4+iOS8SDK,那么在此处设置为0,动画将会不被执行(不确定是哪里的Bug)
- destTransform = CGAffineTransformMakeScale(0.1, 0.1)
- }
- UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
- destView.transform = destTransform
- }, completion: ({completed in
- transitionContext.completeTransition(true)
- }))
- }
上面第一个方法返回动画持续的时间,而下面这个方法才是具体需要实现动画的地方。UIViewControllerAnimatedTransitioning的协议都包含一个对象:transitionContext,通过这个对象能获取到切换时的上下文信息,比如从哪个VC切换到哪个VC等。我们从transitionContext获取containerView,这是一个特殊的容器,切换时的动画将在这个容器中进行;UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey就是从哪个VC切换到哪个VC,容易理解;除此之外,还有直接获取view的UITransitionContextFromViewKey和UITransitionContextToViewKey等。
我按Push和Pop把动画简单的区分了一下,Push时scale由小变大,Pop时scale由大变小,不同的操作,toViewController的视图层次也不一样。最后,在动画完成的时候调用completeTransition,告诉transitionContext你的动画已经结束,这是非常重要的方法,必须调用。在动画结束时没有对containerView的子视图进行清理(比如把fromViewController的view移除掉)是因为transitionContext会自动清理,所以我们无须在额外处理。
注意一点,这样一来会发现原来导航栏的交互式返回效果没有了,如果你想用原来的交互式返回效果的话,在返回动画控制器的delegate方法里返回nil,如:
- if operation == UINavigationControllerOperation.Push {
- navigationOperation = operation
- return self
- }
- return nil
一个简单的自定义导航栏Push/Pop动画就完成了。
自定义Modal的Present/Dismiss动画
- func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {
- return 0.6
- }
- func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {
- let containerView = transitionContext.containerView()
- let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
- let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
- var destView: UIView!
- var destTransfrom = CGAffineTransformIdentity
- let screenHeight = UIScreen.mainScreen().bounds.size.height
- if modalPresentingType == ModalPresentingType.Present {
- destView = toViewController.view
- destView.transform = CGAffineTransformMakeTranslation(0, screenHeight)
- containerView.addSubview(toViewController.view)
- } else if modalPresentingType == ModalPresentingType.Dismiss {
- destView = fromViewController.view
- destTransfrom = CGAffineTransformMakeTranslation(0, screenHeight)
- containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
- }
- UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0,
- options: UIViewAnimationOptions.CurveLinear, animations: {
- destView.transform = destTransfrom
- }, completion: {completed in
- transitionContext.completeTransition(true)
- })
- }
- func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
- modalPresentingType = ModalPresentingType.Present
- return self
- }
- func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
- modalPresentingType = ModalPresentingType.Dismiss
- return self
- }
- override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
- let modal = segue.destinationViewController as UIViewController
- modal.transitioningDelegate = self
- }
自定义导航栏的交互式动画
使用UIPercentDrivenInteractiveTransition
实际上这个类就是实现了UIViewControllerInteractiveTransitioning协议的交互控制器,我们使用它就能够轻松地为动画控制器添加一个交互动画。调用updateInteractiveTransition:更新进度;调用cancelInteractiveTransition取消交互,返回到切换前的状态;调用finishInteractiveTransition通知上下文交互已完成,同completeTransition一样。我们把交互动画应用到详情页面Back回主页面的地方,由于之前的动画管理器的角色是主页面担任的,Navigation Controller的delegate同一时间只能有一个,那在这里交互控制器的角色也由主页面来担任。首先添加一个手势识别器:
- let popRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: Selector("handlePopRecognizer:"))
- popRecognizer.edges = UIRectEdge.Left
- self.navigationController.view.addGestureRecognizer(popRecognizer)
- func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {
- var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width
- progress = min(1.0, max(0.0, progress))
- println("\(progress)")
- if popRecognizer.state == UIGestureRecognizerState.Began {
- println("Began")
- self.interactivePopTransition = UIPercentDrivenInteractiveTransition()
- self.navigationController.popViewControllerAnimated(true)
- } else if popRecognizer.state == UIGestureRecognizerState.Changed {
- self.interactivePopTransition?.updateInteractiveTransition(progress)
- println("Changed")
- } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {
- if progress > 0.5 {
- self.interactivePopTransition?.finishInteractiveTransition()
- } else {
- self.interactivePopTransition?.cancelInteractiveTransition()
- }
- println("Ended || Cancelled")
- self.interactivePopTransition = nil
- }
- }
现在我们已经有了交互控制器对象,只需要把它给告知给Navigation Controller就行了,我们实现UINavigationControllerDelegate的另一个方法:
- func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {
- return self.interactivePopTransition
- }
使用UIPercentDrivenInteractiveTransition的Demo
自定义交互控制器
- func startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning!) {
- self.transitionContext = transitionContext
- let containerView = transitionContext.containerView()
- let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
- let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
- containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
- self.transitingView = fromViewController.view
- }
- func updateWithPercent(percent: CGFloat) {
- let scale = CGFloat(fabsf(Float(percent - CGFloat(1.0))))
- transitingView?.transform = CGAffineTransformMakeScale(scale, scale)
- transitionContext?.updateInteractiveTransition(percent)
- }
- func finishBy(cancelled: Bool) {
- if cancelled {
- UIView.animateWithDuration(0.4, animations: {
- self.transitingView!.transform = CGAffineTransformIdentity
- }, completion: {completed in
- self.transitionContext!.cancelInteractiveTransition()
- self.transitionContext!.completeTransition(false)
- })
- } else {
- UIView.animateWithDuration(0.4, animations: {
- print(self.transitingView)
- self.transitingView!.transform = CGAffineTransformMakeScale(0, 0)
- print(self.transitingView)
- }, completion: {completed in
- self.transitionContext!.finishInteractiveTransition()
- self.transitionContext!.completeTransition(true)
- })
- }
- }
- func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {
- var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width
- progress = min(1.0, max(0.0, progress))
- println("\(progress)")
- if popRecognizer.state == UIGestureRecognizerState.Began {
- println("Began")
- isTransiting = true
- //self.interactivePopTransition = UIPercentDrivenInteractiveTransition()
- self.navigationController.popViewControllerAnimated(true)
- } else if popRecognizer.state == UIGestureRecognizerState.Changed {
- //self.interactivePopTransition?.updateInteractiveTransition(progress)
- updateWithPercent(progress)
- println("Changed")
- } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {
- //if progress > 0.5 {
- // self.interactivePopTransition?.finishInteractiveTransition()
- //} else {
- // self.interactivePopTransition?.cancelInteractiveTransition()
- //}
- finishBy(progress < 0.5)
- println("Ended || Cancelled")
- isTransiting = false
- //self.interactivePopTransition = nil
- }
- }
- func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {
- if !self.isTransiting {
- return nil
- }
- return self
- }