首页 >Swift

iOS 自定义页面的切换动画与交互动画(By Swift)

2014-08-22 14:02 编辑: suiling 分类:Swift 来源:CocoaChina
(via: Bannings的博客
 
在iOS7之前,开发者为了寻求自定义 Navigation Controller 的 Push/Pop 动画,只能受限于子类化一个 UINavigationController,或是用自定义的动画去覆盖它。但是随着iOS 7的到来,Apple针对开发者推出了新的工具,以更灵活地方式管理 UIViewController 切换。
 
我把最终的Demo稍做修改,算是找了一个合适的应用场景,另外配上几张美图,拉拉人气。
 
虽然是Swift的Demo,但是转成Objective-C相当容易。
 
最终效果预览:
自定义导航栏的 Push/Pop 动画
为了在基于 UINavigationController 下做自定义的动画切换,先建立一个简单的工程,这个工程的 rootViewController是一个UINavigationController,UINavigationController的rootViewController是一个简单的UIViewController(称之为主页面),通过这个UIViewController上的一个Button能进入到下一个UIViewController中(称之为详情页面),我们先在主页面的ViewController上实现两个协议:UINavigationControllerDelegate和UIViewControllerAnimatedTransitioning,然后在 ViewDidLoad 里面把 navigationController 的 delegate 设为 self,这样在导航栏 Push 和 Pop 的时候我们就知道了,然后用一个属性记下是 Push 还是 Pop,就像这样:
  1. func navigationController(navigationController: UINavigationController!, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController!, toViewController toVC: UIViewController!) -> UIViewControllerAnimatedTransitioning! {   
  2.     navigationOperation = operation   
  3.     return self   
  4. }   
 
这是iOS7的新方法,这个方法需要你提供一个UIViewControllerAnimatedTransitioning,那UIViewControllerAnimatedTransitioning到底是什么呢?
 
UIViewControllerAnimatedTransitioning 是苹果新增加的一个协议,其目的是在需要使用自定义动画的同时,又不影响视图的其他属性,让你把焦点集中在动画实现的本身上,然后通过在这个协议的回调里编写自定义的动画代码,即“切换中应该会发生什么”,负责切换的具体内容,任何实现了这一协议的对象被称之为动画控制器。你可以借助协议能被任何对象实现的这一特性,从而把各种动画效果封装到不同的类中,只要方便使用和管理,你可以发挥一切手段。我在这里让主页面实现动画控制器也是可以的,因为它是导航栏的 rootViewController ,会一直存在,我只要在里面编写自定义的 Push 和 Pop 动画代码就可以了:
  1. //UIViewControllerTransitioningDelegate   
  2. func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {   
  3.     return 0.4   
  4. }   
  5.    
  6. func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {   
  7.     let containerView = transitionContext.containerView()   
  8.     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)   
  9.     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)   
  10.        
  11.     var destView: UIView!   
  12.     var destTransform: CGAffineTransform!   
  13.     if navigationOperation == UINavigationControllerOperation.Push {   
  14.         containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)   
  15.         destView = toViewController.view   
  16.         destView.transform = CGAffineTransformMakeScale(0.1, 0.1)   
  17.         destTransform = CGAffineTransformMakeScale(1, 1)   
  18.     } else if navigationOperation == UINavigationControllerOperation.Pop {   
  19.         containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)   
  20.         destView = fromViewController.view   
  21.         // 如果IDE是Xcode6 Beta4+iOS8SDK,那么在此处设置为0,动画将不会被执行(不确定是哪里的Bug)   
  22.         destTransform = CGAffineTransformMakeScale(0.1, 0.1)   
  23.     }   
  24.     UIView.animateWithDuration(transitionDuration(transitionContext), animations: {   
  25.             destView.transform = destTransform   
  26.         }, completion: ({completed in   
  27.             transitionContext.completeTransition(true)   
  28.         }))   
  29. }   
 
上面第一个方法返回动画持续的时间,而下面这个方法才是具体需要实现动画的地方。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,如:
  1. if operation == UINavigationControllerOperation.Push {   
  2.     navigationOperation = operation   
  3.     return self   
  4. }   
  5. return nil   
然后在 viewDidLoad 里,Objective-C 直接 self.navigationController.interactivePopGestureRecognizer.delegat = self 就行了,Swift 除了要 navigationController.interactivePopGestureRecognizer.delegate = self 之外,还要在 self 上声明实现了 UIGestureRecognizerDelegate 这个协议,虽然实际上你并没有实现。
 
一个简单的自定义导航栏Push/Pop动画就完成了。
 
自定义Modal的Present/Dismiss动画
自定义 Modal 的 Present 与 Dismiss 动画与之前类似,都需要提供一个动画管理器,我们用详情页面来展示一个 Modal 页面,详情页面就作为动画管理器:
  1. func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {   
  2.     return 0.6   
  3. }   
  4.    
  5. func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {   
  6.     let containerView = transitionContext.containerView()   
  7.        
  8.     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)   
  9.     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)   
  10.        
  11.     var destView: UIView!   
  12.     var destTransfrom = CGAffineTransformIdentity   
  13.     let screenHeight = UIScreen.mainScreen().bounds.size.height   
  14.        
  15.     if modalPresentingType == ModalPresentingType.Present {   
  16.         destView = toViewController.view   
  17.         destView.transform = CGAffineTransformMakeTranslation(0, screenHeight)   
  18.         containerView.addSubview(toViewController.view)   
  19.     } else if modalPresentingType == ModalPresentingType.Dismiss {   
  20.         destView = fromViewController.view   
  21.         destTransfrom = CGAffineTransformMakeTranslation(0, screenHeight)   
  22.         containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)   
  23.     }   
  24.        
  25.     UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0,   
  26.     options: UIViewAnimationOptions.CurveLinear, animations: {   
  27.         destView.transform = destTransfrom   
  28.     }, completion: {completed in   
  29.         transitionContext.completeTransition(true)   
  30.     })   
  31. }   
 
动画部分用了一个iOS7的弹簧动画,usingSpringWithDamping 的值设置得越小,弹的就越明显,动画的其他地方与之前类似,不一样的是之前主页面除了做动画管理器之外,还实现了UINavigationControllerDelegate协议,因为我们是自定义导航栏的动画,而在这里需要自定义 Modal 动画就要实现另一个协议:UIViewControllerTransitioningDelegate,这个协议与之前的 UINavigationControllerDelegate 协议具有相似性,都是返回一个动画管理器,iOS7的方法总共有四个,有两个交互式的先不管,我们只需要实现另两个即可:
  1. func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! {   
  2.     modalPresentingType = ModalPresentingType.Present   
  3.     return self   
  4. }   
  5.    
  6. func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! {   
  7.     modalPresentingType = ModalPresentingType.Dismiss   
  8.     return self   
  9. }   
 
我同样的用一个属性记下是 Present 还是 Dismiss,然后返回 self。因为我是用的 Storyboard,所以需要在 prepareForSegue 方法里设置一下 transitionDelegate:
  1. override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {   
  2.     let modal = segue.destinationViewController as UIViewController   
  3.     modal.transitioningDelegate = self   
  4. }   
对需要执行自定义动画的 VC 设置 transitionDelegate 属性即可。
 
如此一来,一个针对模态 VC 的自定义动画也完成了。
 
自定义导航栏的交互式动画
与动画控制器类似,我们把实现了 UIViewControllerInteractiveTransitioning 协议的对象称之为交互控制器,最常用的就是把交互控制器应用到导航栏的 Back 手势返回上,而如果要实现一个自定义的交互式动画,我们有两种方式来完成:实现一个交互控制器,或者使用iOS提供的 UIPercentDrivenInteractiveTransition 类作交互控制器。
 
使用UIPercentDrivenInteractiveTransition
我们这里就用UIPercentDrivenInteractiveTransition来完成导航栏的交互式动画。先看下 UIPercentDrivenInteractiveTransition 的定义:
实际上这个类就是实现了 UIViewControllerInteractiveTransitioning 协议的交互控制器,我们使用它就能够轻松地为动画控制器添加一个交互动画。调用 updateInteractiveTransition: 更新进度;调用cancelInteractiveTransition 取消交互,返回到切换前的状态;调用 finishInteractiveTransition 通知上下文交互已完成,同 completeTransition 一样。我们把交互动画应用到详情页面Back回主页面的地方,由于之前的动画管理器的角色是主页面担任的,Navigation Controller 的 delegate 同一时间只能有一个,那在这里交互控制器的角色也由主页面来担任。首先添加一个手势识别器:
  1. let popRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: Selector("handlePopRecognizer:"))   
  2. popRecognizer.edges = UIRectEdge.Left   
  3. self.navigationController.view.addGestureRecognizer(popRecognizer)   
  4. UIScreenEdgePanGestureRecognizer继承于UIPanGestureRecognizer,能检测从屏幕边缘滑动的手势,设置edges为left检测左边即可。然后实现handlePopRecognizer: 
  5. func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {   
  6.     var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width   
  7.     progress = min(1.0, max(0.0, progress))   
  8.        
  9.     println("\(progress)")   
  10.     if popRecognizer.state == UIGestureRecognizerState.Began {   
  11.         println("Began")   
  12.         self.interactivePopTransition = UIPercentDrivenInteractiveTransition()   
  13.         self.navigationController.popViewControllerAnimated(true)   
  14.     } else if popRecognizer.state == UIGestureRecognizerState.Changed {   
  15.         self.interactivePopTransition?.updateInteractiveTransition(progress)   
  16.         println("Changed")   
  17.     } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {   
  18.         if progress > 0.5 {   
  19.             self.interactivePopTransition?.finishInteractiveTransition()   
  20.         } else {   
  21.             self.interactivePopTransition?.cancelInteractiveTransition()   
  22.         }   
  23.         println("Ended || Cancelled")   
  24.         self.interactivePopTransition = nil   
  25.     }   
  26. }   
我用了一个实例变量引用 UIPercentDrivenInteractiveTransition,这个类只在需要用时才创建,否则在正常 Push/Pop 的时候,即使只是点击操作并没有识别手势的情况下,也会进入交互(你也可以在要求你返回交互控制器时,进行一些判断,通过返回 nil 来屏蔽,但这显然就太麻烦了)。当手势识别的时候我们调用 pop,用户手势发生变化时,调用 update 去更新,不管是 end 还是 cancel,都判断下是进入下一个页面还是返回之前的页面,完成这一切后把交互控制器清理掉。
 
现在我们已经有了交互控制器对象,只需要把它给告知给Navigation Controller就行了,我们实现 UINavigationControllerDelegate 的另一个方法:
  1. func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {   
  2.     return self.interactivePopTransition   
  3. }   
我们从详情页面通过自定义的交互动画返回到上一个页面的工作就完成了。
 
Demo效果预览:
使用UIPercentDrivenInteractiveTransition的Demo
/cms/uploads/soft/140822/4196-140R2145T5.zip
 
 
自定义交互控制器
我在之前提过,UIPercentDrivenInteractiveTransition 实际上就是实现了 UIViewControllerInteractiveTransitioning 协议,只要是实现了这个协议的对象就可以称之为交互控制器,我们如果想更加精确的管理动画以及深入理解处理上的细节,就需要自己实现 UIViewControllerInteractiveTransitioning协议。
 
UIViewControllerInteractiveTransitioning 协议总共有三个方法,其中 startInteractiveTransition: 是必须实现的方法,我们在里面初始化动画的状态:
  1. func startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning!) {   
  2.     self.transitionContext = transitionContext   
  3.        
  4.     let containerView = transitionContext.containerView()   
  5.     let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)   
  6.     let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)   
  7.        
  8.     containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)   
  9.        
  10.     self.transitingView = fromViewController.view   
  11. }   
这里不涉及动画,只是把需要切换的 view 添加到上下文环境中即可。动画部分我们还是和之前使用 UIPercentDrivenInteractiveTransition 的接口保持一致,添加几个方法:
  1. func updateWithPercent(percent: CGFloat) {   
  2.     let scale = CGFloat(fabsf(Float(percent - CGFloat(1.0))))   
  3.     transitingView?.transform = CGAffineTransformMakeScale(scale, scale)   
  4.     transitionContext?.updateInteractiveTransition(percent)   
  5. }   
  6.    
  7. func finishBy(cancelled: Bool) {   
  8.     if cancelled {   
  9.         UIView.animateWithDuration(0.4, animations: {   
  10.             self.transitingView!.transform = CGAffineTransformIdentity   
  11.         }, completion: {completed in   
  12.             self.transitionContext!.cancelInteractiveTransition()   
  13.             self.transitionContext!.completeTransition(false)   
  14.         })   
  15.     } else {   
  16.         UIView.animateWithDuration(0.4, animations: {   
  17.             print(self.transitingView)   
  18.             self.transitingView!.transform = CGAffineTransformMakeScale(0, 0)   
  19.             print(self.transitingView)   
  20.         }, completion: {completed in   
  21.             self.transitionContext!.finishInteractiveTransition()   
  22.             self.transitionContext!.completeTransition(true)   
  23.         })   
  24.     }   
  25. }   
 
updateWithPercent: 方法用来更新 view 的 transform 属性,finishBy: 方法主要用来判断是进入下一个页面还是返回到之前的页面,并告知 transitionContext 目前的状态,以及对当前正在 scale 的 view 做最后的动画。这里的 transitionContext 和 transitingView 可以在前面的处理手势识别代码中取得,我将里面的代码更新了一下,变成下面这样:
  1. func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {   
  2.     var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width   
  3.     progress = min(1.0, max(0.0, progress))   
  4.        
  5.     println("\(progress)")   
  6.     if popRecognizer.state == UIGestureRecognizerState.Began {   
  7.         println("Began")   
  8.         isTransiting = true   
  9.           //self.interactivePopTransition = UIPercentDrivenInteractiveTransition()   
  10.         self.navigationController.popViewControllerAnimated(true)   
  11.     } else if popRecognizer.state == UIGestureRecognizerState.Changed {   
  12.           //self.interactivePopTransition?.updateInteractiveTransition(progress)   
  13.         updateWithPercent(progress)   
  14.         println("Changed")   
  15.     } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {   
  16.           //if progress > 0.5 {   
  17.           //    self.interactivePopTransition?.finishInteractiveTransition()   
  18.           //} else {   
  19.           //    self.interactivePopTransition?.cancelInteractiveTransition()   
  20.           //}   
  21.         finishBy(progress < 0.5)   
  22.         println("Ended || Cancelled")   
  23.         isTransiting = false   
  24.           //self.interactivePopTransition = nil   
  25.     }   
  26. }   
 
另外还用一个额外布尔值变量 isTransiting 来标识当前是否在手势识别中,这是为了在返回交互控制器的时候,不会在不当的时候返回self:
  1. func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {   
  2.     if !self.isTransiting {   
  3.         return nil   
  4.     }   
  5.     return self   
  6. }   
 
这样一来就完成了自定义交互控制器。可以发现,基本流程与使用 UIPercentDrivenInteractiveTransition 是一致的,UIPercentDrivenInteractiveTransition 主要是帮我们封装了 transitionContext 的初始化以及对它的调用等,只是动画部分需要我们在额外处理一下了。
 
使用自定义交互控制器的Demo
 
最终效果:
我在主页面上多放了几个带 Image 的 Button,在点击 Button 时会将 Button 的 Image 传递到详情页面,详情页面相应的也有一个 UIImageView 用来显示。在主页面初始化动画状态的时候,会生成一个Image的快照来进行动画,要是在以前,我们只能通过 UIGraphics 的 APIs 进行一系列的操作,涉及视图的scale、旋转、透明及渲染到context等,但现在,我们只需要用 iOS 7 的API就行了:
  1. snapshotViewAfterScreenUpdates:afterUpdates   
这个API能帮助我们快速获取一个视图的的快照,afterUpdates 参数表示是否等所有效果应用到该视图之后再获取,如果设置为false,则立即获取;为true则会受到后面对该视图的影响。
 
在动画之前,把主页面和详情页面对应的 Button 和 ImageView 隐藏,然后对快照生成的 View 进行动画,动画用简单的 frame 隐式动画就可以了。
 
最终效果的Demo(GitHub
 
最后附上一张图,这个图比较容易区分那几个名称相近的协议:

 

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:对访问控制与protected的理解
下一篇:如何在iOS 8下使用Swift设计一个自定义的输入法
我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部