自定义控制器转场动画及实现下拉菜单的小Demo

本文为投稿文章,译者:我是乔忘记疯狂


本文翻译总结自AppCoda以下两篇文章:


iOS 7开始,苹果为开发者提供了自定义控制器转场动画相关的API,而实现该功能需要以下三个步骤:

  1. 创建一个类作为动画管理器,该类需继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议,我们在这个类中编写我们的动画执行代码。

  2. 为目标控制器指定转场动画代理,既可以使用上一步创建的动画管理器对象,也可以指定来源控制器作为这个代理。

  3. 实现代理协议中的相应方法,在方法中返回第一步创建的动画管理器对象。

准备工作

下载示例程序,地址在这里。(译注:原文地址需要FQ访问,本人已转存到GitHub上,点击这里。)

示例程序如下图所示,点击导航栏上的Action按钮会modal出一个目标控制器,点击Dismiss按钮会返回来源控制器,只不过现在使用的是系统默认的modal动画,接下来我们就来实现自定义转场动画。

vid01.gif

创建动画管理器

创建一个类名称为CustomPresentAnimationController,继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议。这个协议有两个必须实现的方法,我们的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
     return  2.5
}
 
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
 
     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
     let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
     let containerView = transitionContext.containerView()
     let bounds = UIScreen.mainScreen().bounds
     toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
     containerView.addSubview(toViewController.view)
     
     UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
         fromViewController.view.alpha = 0.5
         toViewController.view.frame = finalFrameForVC
         }, completion: {
             finished  in
             transitionContext.completeTransition( true )
             fromViewController.view.alpha = 1.0
     })
}

第一个方法很简单,设定动画执行时间。第二个方法则用来编写我们自定义的动画代码,在这个方法中我们可以利用transitionContext(转场上下文)来获得我们将来的来源控制器、目标控制器、动画完成后的最终frame,还可以获得用来管理来源或目标视图的容器视图。

然后我们将目标视图调整到屏幕下方并将其添加到容器视图内。接下来在动画执行的闭包内,将目标视图的位置变为最终位置,并将来源视图的透明度降为0.5,使其在目标视图进入的过程中产生一个淡出的效果。在动画完成的闭包内,我们告知transitionContext动画已完成,并将来源视图的透明度改回1.0。

设置转场动画代理

接下来我们需要为目标控制器设置转场动画代理,这里我们指定来源控制器作为我们的代理。在ItemsTableViewController中,让其遵守UIViewControllerTransitioningDelegate协议,在storyboard中找到我们modal的segue,设置它的Identifier为showAction。然后在ItemsTableViewController中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
let customPresentAnimationController = CustomPresentAnimationController()
 
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
 
     if  segue.identifier ==  "showAction"  {
         let toViewController = segue.destinationViewController as UIViewController
         toViewController.transitioningDelegate = self
     }
}
 
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     return  customPresentAnimationController
}

我们创建了一个动画管理器对象,设置目标控制器的转场代理为来源控制器,然后实现代理协议中的animationControllerForPresentedController方法,该方法用于指定modal过程中展示视图的动画,在该方法中返回我们自定义的动画管理器对象。

运行我们的程序,效果如下图所示:

59.gif

跟系统默认modal效果差不多,不过带有弹簧效果。如果你希望有不同的效果,你可以对下面这句代码进行修改。

1
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)

比如将其改为如下代码:

1
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height)

再次运行程序,我们的modal动画就变为从上往下了。

vid03.gif

自定义modal过程中退出视图的动画

我们的程序现在点击Dismiss退出目标控制器时,仍然是系统默认的动画,接下来实现这个自定义动画。

步骤同前面基本一样,创建一个叫做CustomDismissAnimationController的动画管理器,实现如下代理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
     return  2
}
 
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
     let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
     let containerView = transitionContext.containerView()
     toViewController.view.frame = finalFrameForVC
     toViewController.view.alpha = 0.5
     containerView.addSubview(toViewController.view)
     containerView.sendSubviewToBack(toViewController.view)
     
     UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
         fromViewController.view.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
         toViewController.view.alpha = 1.0
     }, completion: {
         finished  in
         transitionContext.completeTransition( true )
     })
}

这次我们使用一个新的动画方式,让来源视图从中心点开始逐渐变小直到消失。首先我们将目标控制器设置为最终位置,透明度为0.5,并将其添加到容器视图的底层中使其开始时不可见。在动画执行过程中,来源视图逐渐变小,露出底层的目标视图,并将目标视图透明度过渡到1.0。

接下来在ItemsTableViewController中添加如下代码:

1
2
3
4
5
let customDismissAnimationController = CustomDismissAnimationController()
 
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     return  customDismissAnimationController
}

animationControllerForDismissedController这个代理方法指定了modal过程中退出视图的动画。运行程序,你会发现我们的动画有点小Bug。

60.gif

我们可以看到,白色的背景视图确实如我们所愿从中心点逐渐缩小,但是图片视图的大小却保持不变,这是因为改变来源视图的时候,它的子控件的大小并不会跟着发生改变,我们可以通过视图快照的技术来解决这一问题。

将animateTransition方法的实现修改为如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
     let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
     let containerView = transitionContext.containerView()
     toViewController.view.frame = finalFrameForVC
     toViewController.view.alpha = 0.5
     containerView.addSubview(toViewController.view)
     containerView.sendSubviewToBack(toViewController.view)
     
     let snapshotView = fromViewController.view.snapshotViewAfterScreenUpdates( false )
     snapshotView.frame = fromViewController.view.frame
     containerView.addSubview(snapshotView)
     
     fromViewController.view.removeFromSuperview()
     
     UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
         snapshotView.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
         toViewController.view.alpha = 1.0
     }, completion: {
         finished  in
         snapshotView.removeFromSuperview()
         transitionContext.completeTransition( true )
     })  
}

我们给来源视图生成了一个快照,将它添加到容器视图中利用它来做动画,并将来源视图从父控件中移除。再次运行程序,我们的动画效果就正常了。

vid06.gif

导航控制器的转场动画

在UITabBarController和UINavigationController的管理下,你无需为每个目标控制器都设置转场代理,可以直接设置UITabBarControllerDelegate或UINavigationControllerDelegate即可。

接下来我们演示如何为导航控制器设置自定义转场动画。首先,仍然是创建一个动画管理器类叫做CustomNavigationAnimationController,然后实现UIViewControllerAnimatedTransitioning协议的方法。这里的动画代码采用的是一个开源的三维旋转动画,读者可以到这里自行研究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var  reverse: Bool =  false
 
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
     return  1.5
}
 
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
     let containerView = transitionContext.containerView()
     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
     let toView = toViewController.view
     let fromView = fromViewController.view
     let direction: CGFloat = reverse ? -1 : 1
     let const: CGFloat = -0.005
     
     toView.layer.anchorPoint = CGPointMake(direction == 1 ? 0 : 1, 0.5)
     fromView.layer.anchorPoint = CGPointMake(direction == 1 ? 1 : 0, 0.5)
     
     var  viewFromTransform: CATransform3D = CATransform3DMakeRotation(direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0)
     var  viewToTransform: CATransform3D = CATransform3DMakeRotation(-direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0)
     viewFromTransform.m34 = const
     viewToTransform.m34 = const
     
     containerView.transform = CGAffineTransformMakeTranslation(direction * containerView.frame.size.width / 2.0, 0)
     toView.layer.transform = viewToTransform
     containerView.addSubview(toView)
     
     UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
         containerView.transform = CGAffineTransformMakeTranslation(-direction * containerView.frame.size.width / 2.0, 0)
         fromView.layer.transform = viewFromTransform
         toView.layer.transform = CATransform3DIdentity
     }, completion: {
         finished  in
         containerView.transform = CGAffineTransformIdentity
         fromView.layer.transform = CATransform3DIdentity
         toView.layer.transform = CATransform3DIdentity
         fromView.layer.anchorPoint = CGPointMake(0.5, 0.5)
         toView.layer.anchorPoint = CGPointMake(0.5, 0.5)
         
         if  (transitionContext.transitionWasCancelled()) {
             toView.removeFromSuperview()
         else  {
             fromView.removeFromSuperview()
         }
         transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
     })        
}

注意这里我们添加了一个reverse变量,用来指定转场动画的方向,这样我们可以将导航控制器push和pop过程的动画封装在一个动画管理器中。

在ItemsTableViewController中更改它的声明使其遵守UINavigationControllerDelegate协议,在viewDidLoad方法中设置代理为自己navigationController?.delegate = self,然后添加如下代码:

1
2
3
4
5
6
let customNavigationAnimationController = CustomNavigationAnimationController()
 
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     customNavigationAnimationController.reverse = operation == .Pop
     return  customNavigationAnimationController
}

上面这个导航控制器的代理方法用于指定push或pop时的转场动画,其中operation参数可以用来判断转场的方向。运行程序,如下图所示:

61.gif

导航控制器的手势交互

我们知道苹果官方为导航控制器添加了一个默认的手势交互,就是在屏幕左侧向右滑动可以返回上一界面并带有pop动画,接下来我们为我们的自定义动画添加手势交互。

手势交互的管理器需要遵守的是UIViewControllerInteractiveTransitioning协议,该协议需要实现startInteractiveTransition方法指定开始交互,不过苹果官方为我们提供了另一个已经实现该协议的交互管理器类UIPercentDrivenInteractiveTransition,并提供以百分比的形式来控制交互过程的功能,比如控制交互的更新、取消、完成等,我们直接使用它来实现我们的交互控制。

创建一个类叫做CustomInteractionController并继承自UIPercentDrivenInteractiveTransition,添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var  navigationController: UINavigationController!
var  shouldCompleteTransition =  false
var  transitionInProgress =  false
var  completionSeed: CGFloat {
     return  1 - percentComplete
}
 
func attachToViewController(viewController: UIViewController) {
     navigationController = viewController.navigationController
     setupGestureRecognizer(viewController.view)
}
 
private func setupGestureRecognizer(view: UIView) {
         view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action:  "handlePanGesture:" ))
}
 
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
     let viewTranslation = gestureRecognizer.translationInView(gestureRecognizer.view!.superview!)
     switch  gestureRecognizer.state {
     case  .Began:
         transitionInProgress =  true
         navigationController.popViewControllerAnimated( true )
     case  .Changed:
         var  const = CGFloat(fminf(fmaxf(Float(viewTranslation.x / 200.0), 0.0), 1.0))
         shouldCompleteTransition = const > 0.5
         updateInteractiveTransition(const)
     case  .Cancelled, .Ended:
         transitionInProgress =  false
         if  !shouldCompleteTransition || gestureRecognizer.state == .Cancelled {
             cancelInteractiveTransition()
         else  {
             finishInteractiveTransition()
         }
     default :
         println( "Swift switch must be exhaustive, thus the default" )
     }
}

attachToViewController方法用于将来传入导航控制器的目标控制器,我们为目标控制器的整个view添加了滑动手势以便将来可以实现滑动返回的pop动画,在监听手势滑动的方法中,我们根据手势的状态做如下处理:

  • 开始滑动:设置transitionInProgress为true,并开始执行导航控制器的pop返回。

  • 滑动过程中:更新交互过程的百分比,我们假设指定滑动200点即为交互完成。

  • 取消或结束:设置transitionInProgress为false,如果交互过程执行50%以上则认为交互完成。

接来下来到我们的ItemsTableViewController,添加如下代码:

1
let customInteractionController = CustomInteractionController()

然后修改我们之前实现的导航控制器的代理方法如下:

1
2
3
4
5
6
7
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
     if  operation == .Push {
         customInteractionController.attachToViewController(toVC)
     }
     customNavigationAnimationController.reverse = operation == .Pop
     return  customNavigationAnimationController
}

当我们push一个目标控制器时,就为该目标控制器设定交互控制。最后实现导航控制器代理中的另一个方法用于指定交互控制器,代码如下:

1
2
3
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
     return  customInteractionController.transitionInProgress ? customInteractionController : nil
}

运行程序,如下图所示:

62.gif

完整的示例程序链接地址请点击这里

推荐阅读:

objc中国:自定义 ViewController 容器转场

喵神的好文:WWDC 2013 Session笔记 - iOS7中的ViewController切换

实现下拉菜单的小Demo

Demo实现效果如下图所示,下载完整的Demo代码请点击这里。(译注:原文地址需要FQ访问,本人已转存到GitHub上,点击这里。)

slide-down-menu-optimize.gif

实现过程同我们前面讲的自定义转场动画过程一样,首先创建一个动画管理器类MenuTransitionManager,然后设置目标控制器的转场代理,这次我们使用动画管理器对象作为代理,所以MenuTransitionManager既遵守了UIViewControllerAnimatedTransitioning协议,也遵守了UIViewControllerTransitioningDelegate协议。动画的执行代码比较简单,只是通过改变transform控制来源和目标视图的上下移动,目标视图我们仍然使用了快照技术。

我们还为来源视图的快照添加了一个点击的手势,这样在显示下拉菜单后,除了点击相应的菜单选项,点击下部的快照也可以返回到主页视图。只不过点击手势的处理我们使用了代理设计模式,而点击手势的添加我们使用了Swift的属性观察器语法,读者可以自行研究学习。

最后,希望大家学的愉快!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值