iOSUIAnimation

自定义navigation controller过渡动画

2016-02-15  本文已影响1543人  TifaTsubasa

除了导航控制器自带的左右滑动的过渡动画,如何去自定义一个特殊的导航过渡效果呢
源码见Github

设计思路

缩放过渡的思路其实非常简单,在push/pop过程中,设置上一层控制器view的scale就营造出下沉的效果,重点是

如何控制导航过渡的过程

步骤

1.初始化控制器

TTScaleNavigationController.swift // 继承UINavigationController用来重写导航的动画设置
TTScaleFirstController.swift    // 导航的前一页控制器
TTScaleSecondController.swift   // 导航的后一页控制器

首先需要初始化一个导航控制器和两个ViewController做基本的push/pop,当然,只有默认的左右滑动的效果😁

2.设置过渡动画

在什么地方控制转场的过程,如何修改转场动画呢?

这里就要用到iOS7新增的API,苹果提供的自定义转场动画的协议UIViewControllerAnimatedTransitioning,负责管理在转场切换过程发生的事件

以自定义push转场为例:

1. 创建过场动画的接口类

首先我们需要一个继承于NSObject,遵守UIViewControllerAnimatedTransitioning协议的类

class TTPushTransition: NSObject, UIViewControllerAnimatedTransitioning {

}
2. 设置过场时间

TTPushTransition中使用协议中的方法设置过场时间为1s(正常的过场时间大约为0.3s,1s用于测试)

func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
    return 1
}
3. 设置push过场动画

首先我们需要理清一下过场动画的流程:

过场的动画,需要在UIViewControllerAnimatedTransitioning提供的

public func animateTransition(transitionContext: UIViewControllerContextTransitioning)

方法内实现,方法中的过场上下文transitionContext,会提供设置动画的所需的各个对象

// 使用对应key取得相应控制器
let fromVc = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toVc = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

let duration = self.transitionDuration(transitionContext)   // 根据另一协议方法获得过场时间
let containerView = transitionContext.containerView()   // 过场容器视图

过场上下文提供了一个容器视图containerView,在过场过程中,这个视图就相当一个舞台,fromVc和toVc的View可以在容器内做各种动画,首先我们需要准备一下舞台

containerView?.addSubview(toVc!.view)   // 默认fromVc的视图已经加入容器内
let screenW = UIScreen.mainScreen().bounds.width
let screenH = UIScreen.mainScreen().bounds.height
toVc?.view.frame = CGRectMake(screenW, 0, screenW, screenH)

设置好toVc的视图位置后,就开始正式的动画设置了

UIView.animateWithDuration(duration, animations: { () -> Void in
    fromVc?.view.transform = CGAffineTransformMakeScale(0.7, 0.7) // fromVc视图的scale设置到0.7
    toVc?.view.frame = CGRectMake(0, 0, screenW, screenH)   // toVc视图从屏幕右方移动到屏幕中间
    }) { (_) -> Void in
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    }

需要注意的是,在动画结束时,必须调用transitionContext.completeTransition(!transitionContext.transitionWasCancelled())来清理舞台,如果传入一个true好像也没有问题,但是在加入右划返回手势,滑动一半取消时,就会出现问题,因此需要根据过场是否被取消来正确清理过场上下文

4. 更改导航的方式

完成上面三步,我们就写好了过场的剧本,接下来就得请演员TTScaleNavigationController上台表演了

import UIKit

class TTScaleNavigationController: UINavigationController, UINavigationControllerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
    }

    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if (operation == .Push) {
            return TTPushTransition()
        }
        return nil
    }
}

在UINavigationControllerDelegate的方法中,设置push状态的过场是写好的"剧本",编译运行一次,push的动画已经像模像样了,不过好像只会生效一次o(╯□╰)o,需要完善pop的动画

5. 设置pop的动画

设置pop动画的流程跟push类似,也需要新建一个遵守UIViewControllerAnimatedTransitioning的NSObject类,唯一不同的是设置动画,但实际上也就是push动画的逆向,这里就直接贴上代码了

import UIKit

class TTPopTransition: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 1
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView()
        let fromVc = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
        let toVc = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

        let duration = self.transitionDuration(transitionContext)
        let screenW = UIScreen.mainScreen().bounds.width
        let screenH = UIScreen.mainScreen().bounds.height

        containerView?.addSubview(toVc!.view)
        containerView?.sendSubviewToBack(toVc!.view)
        UIView.animateWithDuration(duration, animations: { () -> Void in
            fromVc?.view.frame = CGRectMake(screenW, 0, screenW, screenH)
            toVc?.view.transform = CGAffineTransformMakeScale(1, 1)
            }) { (_) -> Void in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        }
    }
}

然后完善一下TTScaleNavigationController的协议方法,补充pop状态需要的过场

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    if (operation == .Push) {
        return TTPushTransition()
    } else if operation == .Pop {
        return TTPopTransition()
    }
    return nil
}

再次运行项目,导航的过场已经基本完善了,已经拥有了想象动画,但是!!!

6. 右划返回手势

重写了导航控制器之后,右划手势就失效了,需要手动添加手势😔

回到TTScaleNavigationController中,手势需要一个新的对象来记录手势状态,并且这个对象最终会通知导航进行相应操作,添加

var interactivePopTransition: UIPercentDrivenInteractiveTransition?

然后在viewDidLoad方法中添加边缘手势

let popRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: "handlePopRecognizer:")
popRecognizer.edges = .Left;
self.view.addGestureRecognizer(popRecognizer)

记得补充手势的响应方法,这里使用了Swift的switch特性来判断状态并记录状态

func handlePopRecognizer(recognizer: UIScreenEdgePanGestureRecognizer) {
    // 获取手势在屏幕横屏范围的滑动百分比,并控制在0.0 - 1.0之间
    var progress = recognizer.translationInView(self.view).x / self.view.bounds.width
    progress = min(1.0, max(0.0, progress))

    switch recognizer.state {
    case .Began:    // 开始滑动:初始化UIPercentDrivenInteractiveTransition对象,并开启导航pop
        interactivePopTransition = UIPercentDrivenInteractiveTransition()
        self.popViewControllerAnimated(true)
    case .Changed:  // 滑动过程中,根据在屏幕上滑动的百分比更新状态
        interactivePopTransition?.updateInteractiveTransition(progress)
    case .Ended, .Cancelled:    // 滑动结束或取消时,判断手指位置,在左半屏幕取消pop,在右半屏幕完成pop过程
        if progress > 0.5 {
            interactivePopTransition?.finishInteractiveTransition()
        } else {
            interactivePopTransition?.cancelInteractiveTransition()
        }
        interactivePopTransition = nil
    default: break
    }
}

最后,还要把我们记录下来的UIPercentDrivenInteractiveTransition对象通知给导航控制器

func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return interactivePopTransition
}

终于,一个自定义过场方式的导航控制器就完活儿了😉

优化

1. 添加阴影

为toVc的视图添加左侧的阴影,提高两个视图的层次感


TTPushTransition的动画设置方法中,添加
// shadows
toVc?.view.layer.shadowOffset = CGSizeMake(-3, 0);
toVc?.view.layer.shadowColor = UIColor.blackColor().colorWithAlphaComponent(0.3).CGColor
toVc?.view.layer.shadowOpacity = 1
2. 渐亮渐暗效果

为fromVc提供push渐暗,pop渐亮的效果

思路是在fromVc和toVc的视图中间,插入一层黑色的view,并调节这一view的透明度,在TTPushTransition的动画设置方法中,在动画开始前插入蒙版视图

let blackView = UIView(frame: CGRectMake(0, 0, screenW, screenH))
blackView.backgroundColor = UIColor.blackColor()
blackView.alpha = 0
containerView?.insertSubview(blackView, belowSubview: toVc!.view)

在动画中,设置blackView.alpha = 0.7并在动画结束时blackView.removeFromSuperview()
pop过程自然就是一个相反的过程了,同样插入一个蒙版透明度从0.7到0

3. 优化参数

记得修改动画时间到0.3,fromVc视图的scale为0.95 😜


如果你也喜爱游戏,欢迎支持我的APP Up 游戏专辑

上一篇 下一篇

猜你喜欢

热点阅读