iOS 转场动画探究(一)

什么是转场动画

       转场动画说的直接点就是你常见的界面跳转的时候看到的动画效果,我们比较常见的就是控制器之间的Push和Pop,还有Present和Dismiss的时候设置一下系统给我们的modalTransitionStyle,以及通过手势的左滑或者是右滑的转场等等,这些就是我们比较常见的,当然很大部分APP转场的方式也是我们上面说的常见的。我自己的建议和理解,转场动画能帮你加深理解、总结你对动画的学习,但不要轻易在你的项目中大量的去尝试,还是觉得动画用的好就有点睛之笔的感觉,但若是大量的使用,很容易给人造成审美和视觉疲劳。当然这个可能就是对你们设计或者是产品功力的考验了。要他们真的做出了点睛的动画也是希望我们搞的定。

      我们要说的肯定就不是我们常见的转场了,在那些特殊的转场动画面前我们应该怎么做。针对这问题,我们分开一步一步的解析,不想让篇幅太长了,我们在这里会分成两篇,第一篇主要说理论的地方和穿插一些小案例,第二篇我会把自己最近学习过得一下案例最好全都分享给大家,一起学习。

       

 一:Transition(n. 过渡;转变;[分子生物] 转换;变调

      这个单词估计就是我们转场的基础了,留给英文可能不是那么6的你我他。在下面你肯定会大量的看到它,对于这个Transition(转场)过程中视图控制器和其对应的视图在结构上的变化我在巧神的博客中看到这张图,说实话,不太理解这张图表达了的是什么,把这张图给大家分享出来,你要理解的话可以留言大家讨论一下,接下来先说说在理解转场之前我们需要理解的几个概念:

 

 

*** 官方支持以下几种方式的自定义转场:

         1、我们最常见的在 UINavigationController 中 push 和 pop;

         2、也是比较常见的在 UITabBarController 中切换 Tab;

         3、Modal 转场:presentation 和 dismissal,俗称视图控制器的模态显示和消失,仅限于modalPresentationStyle属性为 UIModalPresentationFullScreen 或 UIModalPresentationCustom 这两种模式,这里要区分上面说的modalTransitionStyle,下面会区分这两个属性。

         4、UICollectionViewController 的布局转场:UICollectionViewController 与 UINavigationController 结合的转场方式;

 

*** 区分modalTransitionStyle和modalPresentationStyle,首先它们俩都是UIViewController的属性: 

       

        1、先说说  modalTransitionStyle,这个是控制器跳转时系统给的几个动画风格,这个在iPhone上用的比较多:

  typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
         
             // 默认的从下到上
             UIModalTransitionStyleCoverVertical = 0,
             // 翻转
             UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,
             // 渐显
             UIModalTransitionStyleCrossDissolve,
             // 类似你翻书时候的效果
             UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
         };

       

        2、再说说modalPresentationStyle,这个是弹出时控制器的风格,modalPresentationStyle的分割在iPad上面统统有效,但在iPhone和iPod touch上面系统始终已UIModalPresentationFullScreen模式显示presentedController,关于modalPresentationStyle在下面也通过注释说一下:

typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
                         
          //presented控制器充满全屏,如果弹出VC的wantsFullScreenLayout设置为YES的,则会填充到状态栏下边,否则不会填充到状态栏之下.iPhone默认是这个
          UIModalPresentationFullScreen = 0,
                         
          //presented控制器的高度和当前屏幕高度相同,宽度和竖屏模式下屏幕宽度相同,剩余未覆盖区域将会变暗并阻止用户点击,这种弹出模式下,竖屏时跟UIModalPresentationFullScreen的效果一样,横屏时候两边则会留下变暗的区域
          UIModalPresentationPageSheet NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
                         
          //presented控制器的高度和宽度均会小于屏幕尺寸,presented VC居中显示,四周留下变暗区域。
          UIModalPresentationFormSheet NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
                         
          //presented控制器的弹出方式和presenting VC的父VC的方式相同。
          UIModalPresentationCurrentContext NS_ENUM_AVAILABLE_IOS(3_2),
                         
          //自定义
          UIModalPresentationCustom NS_ENUM_AVAILABLE_IOS(7_0),
                         
          UIModalPresentationOverFullScreen NS_ENUM_AVAILABLE_IOS(8_0),        
          UIModalPresentationOverCurrentContext NS_ENUM_AVAILABLE_IOS(8_0),
          // http://www.15yan.com/story/jlkJnPmVGzc/ 在iPad上弹出控制器
          UIModalPresentationPopover NS_ENUM_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED,
          // None
          UIModalPresentationNone NS_ENUM_AVAILABLE_IOS(7_0) = -1,        
 };

 

*** Presented和Presenting  fromView和toView

     这个借助于我看博客的时候看到的同行总结的比较好的一句话和图示说明来说一下:

     Presented和Presenting是一组相对的概念,它不受present或dismiss的影响,如果是从A视图控制器present到B,那么A总是B的presentingViewController,B总是A的presentedViewController

     顺便借助于这张图示说明,我们还可以理解一下fromView和toView这个两个概念:

     fromView表示当前视图toView表示要跳转到的视图。如果是从A视图控制器present到B,则A是fromView,B是toView。从B视图控制器dismiss到A时,B变成了fromView,A是toView。在后面在参考博客中我都会把这些博客链接总结发出来。

 

 二:转场的几个关键点

      转场最关键的地方就是几个转场协议,我们分开一个一个的说这几个转场的协议,在说这几个协议的过程中穿插一些简单的转场动画的案列,这些例子最后都会上传到git上去。

 

1、   转场协议:UIViewControllerTransitioningDelegate

       这个协议里面有五个方法,先看看这五个方法,然后把这几个方法逐个解析一下:

       先给大家再普及一个单词!哈哈...最后两个方法有这个interaction( 相互作用;[数] 交互作用),你就理解它就是交互

 

** 下面是这几个方法的代码的注释,它的一些注意的地方以及一些解释在下面代码的注释中有,看了下面的方法,我们也就大概掌握了这个协议:

#pragma mark - UIViewControllerTransitioningDelegate
/*
 不管是 present 还是dismiss
 要是调用interactionControllerForPresentation 或者是 interactionControllerForDismissal
 返回值是nil,就会走下面animationControllerForPresentedController和animationControllerForDismissedController方法
 要是不是nil,就不会走下面这两个方法了, 在我们这里也就是用手势测试的时候是不会走的,点击present或   者是dismiss会走
 */
// 这个方法返回一个遵守 <UIViewControllerAnimatedTransitioning> 协议的对象
// 其实返回的就是PresentedController控制器的动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
      
        //ForithmAnimation这个类可以在Demo中去看,下面我们也会说
        return [ForithmAnimation new];
}

// 这个方法和上面的解释是类似的,只不过这里的控制器就是DismissedController
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{

        //ForithmAnimation 遵守 UIViewControllerAnimatedTransitioning 协议
        return [ForithmAnimation new];
}

// UIKit还会调用代理的interactionControllerForPresentation:方法来获取交互式控制器,如果得到了nil则执行非交互式动画
// 如果获取到了不是nil的对象,那么UIKit不会调用animator的animateTransition方法,而是调用交互式控制器的startInteractiveTransition:方法。
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator{};

// 这个方法是在dismiss的时候的时候调用,也是交互转场执行的时候
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{};

// 这个方法的返回值是UIPresentationController
// UIPresentationController提供了四个函数来定义present和dismiss动画开始前后的操作,这个我们在下面再具体的详细说
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0){};

 

我们接着我说第二点动画协议,这两个说完了,我们说一个简单的实例. 

 

2、 动画协议: UIViewControllerAnimatedTransitioning

      还是老办法,我们要用的它的话我们得先了解它都有些什么东西,点进去看看它里面的方法,把方法也解释一下:

 

 

       这个协议看的出来还是很简单的,终于不用那么长了是吗?哈哈.....

       这两个方法我们就不在代码里面添加注释说明了,在这里一句话描述一下:

       a:  第一个方法是返回动画执行的一个时间,建议设置在0.5以内吧。

       b:  核心方法,转场动画我们就是在这个方法里面添加的,所以,一般讲动画的文章,转场动画都会在最后说说,因为它需要基本动画作为一个基础。

 

3、 转场环境协议 UIViewControllerContextTransitioning

      不知道你有没有注意到上面我们说的 UIViewControllerAnimatedTransitioning 协议的第二个方法里面,有个参数叫transitionContext 它的类型呢?它的最主要的作用就是获取到转场上下文,在接下来的例子中,大家注意下这个方法:

      - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext

      在它里面最开始的时候,我们都会去获取 fromViewController、或者是toViewController亦或是后面的fromView、toView等这些内容,还有contentView,这些就是他最主要的作用。

     

EXAMPLE-ONE: 

      下面的GIF实例分为三个,我们用我们上面说的第一点个第二点要素就能完成的是第一种,逐渐显示,第二种的话需要我们接下来要说的第三点交互控制器协议方法面的东西,我们就在下面第三点说完再说;

 

      这里是Demo的下载地址

      这里是我学习这些内容的原文的博客的地址大家可以去看看原文,原文链接Demo还有Swift版本的Demo给大家,感谢作者!

      Demo的下载地址我在这里给大家,我们现在说的就先是第一种:逐渐出现的转场

      前面的用UICollectionView写的那个圈圈,哈哈.....圈圈代码在ViewController里面,重要的其实就是每一个attributes的center属性,很简答的就不说了,相信大家也都懂。

      重点代码我们说说,在这里说过的我们在下面的代码中就会一笔带过不在解释了:

   

// 点击跳转事件
-(void)presentNextControllerClicked{

        // 跳转到这个控制器ForithmToViewController,当然是继承与UIViewController
        ForithmToViewController * toViewController =[[ForithmToViewController alloc]init];
        toViewController.modalPresentationStyle  = UIModalPresentationFullScreen;
        
        // NOTE:转场的关键就是这个代理 transitioningDelegate
        // 指定了这个代理就需要遵守UIViewControllerTransitioningDelegate这个协议
        // 协议里面的东西点进去可以仔细看看,我们指定toViewController的transitioningDelegate是我们的ForithmFromViewController,也就是
        // fromViewController,这样我们的fromViewController就要遵守这个协议
        toViewController.transitioningDelegate = self;
        [self presentViewController:toViewController animated:YES completion:nil];
}


#pragma mark - UIViewControllerTransitioningDelegate
// 这个方法返回一个遵守 <UIViewControllerAnimatedTransitioning> 协议的对象
// 其实返回的就是PresentedController控制器的动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{

        return [ForithmAnimation new];
}
// 这个方法和上面的解释是类似的,只不过这里的控制器就是DismissedController
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{

        //ForithmAnimation 遵守 UIViewControllerAnimatedTransitioning 协议
        return [ForithmAnimation new];
}

  

       现在这个重点就落在我们这儿,遵守了UIViewControllerAnimatedTransitioning协议的ForithmAnimation:顺便也提一下这就是转场动画API强大的地方,它们都是一些协议API,不管你是谁,只要遵守这些个协议就OK,便捷了许多。也利于我们封装,这也就是那些第三方的转场库你拿来就能直接用的原因。

       接着说我们的ForithmAnimation,让它遵守<UIViewControllerAnimatedTransitioning>,我们看下协议的方法,你可以看到重点全都在我们上面说的动画方法里面:

// 这个方法简单,就是转场动画执行的时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{

     return 0.35;
}

// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{

        // fromViewController
        UIViewController * fromViewController = [transitionContext viewControllerForKey:( UITransitionContextFromViewControllerKey)];
        // toViewController
        UIViewController * toViewController = [transitionContext viewControllerForKey:( UITransitionContextToViewControllerKey)];
        
        /*
         typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
         
             // 默认的从下到上
             UIModalTransitionStyleCoverVertical = 0,
             // 翻转
             UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,
             // 渐显
             UIModalTransitionStyleCrossDissolve,
             // 类似你翻书时候的效果
             UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
         };*/
        //
        UIView * contentView = [transitionContext containerView];
        
        UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        UIView *toView   = [transitionContext viewForKey:UITransitionContextToViewKey];
        
        fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
        toView.frame   = [transitionContext finalFrameForViewController:toViewController];
        fromView.alpha = 1.0f;
        toView.alpha   = 0.0f;

        // 在present和,dismiss时,必须将toview添加到视图层次中
        [contentView addSubview:toView];
        
        // 获取执行时长
        NSTimeInterval transitionDuration = [self transitionDuration:transitionContext];
        [UIView animateWithDuration:transitionDuration animations:^{
           
                fromView.alpha = 0.0f;
                toView.alpha   = 1.0;
                
        } completion:^(BOOL finished) {
             
                //transitionWasCancelled 这个方法判断转场是否已经取消了,下面的completeTransition设置转场完成
                //动画结束后一定要调用completeTransition方法
                //通过transitionWasCancelled()方法来获取转场的状态,使用completeTransition:来完成或取消转场。
                BOOL wasCancelled = [transitionContext transitionWasCancelled];
                [transitionContext completeTransition:!wasCancelled];
        }];
}

      上面方法,一个简单的自定义转场我们就完成了,明白了上面这第一点个第二点的要素,理解这个转场相信对你也不是什么问题,我们接着往下说。

   

4、 交互控制器协议 UIViewControllerInteractiveTransitioning

      说这个UIViewControllerInteractiveTransitioning你就得先知道这个UIPercentDrivenInteractiveTransition,官方的链接给大家,先看的可以去看看,这是一个实现了UIViewControllerInteractiveTransitioning接口的类,为我们预先实现和提供了一系列便利的方法,可以用一个百分比来控制交互式切换的过程。利用手势来完成这个转场,UIPercentDrivenInteractiveTransition为我们提供了很大的便利:

      为了我们的篇幅考虑,不想一篇太长了,不然真的会没有耐心看下去,我们在这里就简单看看这个UIPercentDrivenInteractiveTransition的一些方法,它里面的一些属性什么的我们就不说了,大家可以自己去看看:

     

     

    它里面的方法就这四个,简单说下这四个方法:

            a: 第一个方法是暂停交互 

            b: 第二个是更新方法,一般交互时候的进度更新就在这个方法里面

            c: 第三个是取消交互

            d: 第四个的话就是设置交互完成

 

EXAMPLE-TWO 

现在来说第二个转场的实现:

       1、这个转场需要一个最基本的 UIScreenEdgePanGestureRecognizer 手势,它是一个屏幕边缘滑动手势,这个手势是继承自UIPanGestureRecognizer滑动手势的。这个是手势说一点,就是它的 edges 属性,你要往左边拉动转场的话你就需要设置这个属性为UIRectEdgeRight,一个很简单的理解就是往左边拉动你需要设置它相应右边的滑动手势,这样理解就OK。

        //UIScreenEdgePanGestureRecognizer:UIPanGestureRecognizer
        //添加屏幕边缘滑动手势
        UIScreenEdgePanGestureRecognizer * interactiveTransitionRecognizer;
        interactiveTransitionRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(interactiveTransitionRecognizerAction:)];
        // 响应右边的滑动事件
        interactiveTransitionRecognizer.edges = UIRectEdgeRight;
        [self.view addGestureRecognizer:interactiveTransitionRecognizer];

     

       2、接下来就按照我们说的上面的第一个转场的理解,设置我们 toViewController 的 transitioningDelegate 代理,接下来的是就需要我们再这个代理中去看了,大家先别着急去看,想一想,我们说的代理那五个方法里面和交互有关的两个,前面我们说过,设置了交互就不走动画方法了,交互哪里你需要返回的就是一个上面我们说的遵守UIViewControllerInteractiveTransitioning协议的类,这时候上面说的UIPercentDrivenInteractiveTransition就华丽的出场了,注意下面这个方法,当然这是Presentation,我们的Dismissal也是大家可以去Demo里面看,道理是一样的:

// UIKit还会调用代理的interactionControllerForPresentation:方法来获取交互式控制器,如果得到了nil则执行非交互式动画
// 如果获取到了不是nil的对象,那么UIKit不会调用animator的animateTransition方法,而是调用交互式控制器的startInteractiveTransition:方法。

// interaction 交互
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator
{
     
        // 说说这里的SwipTransitionInteractionController 控制器,
        // 它是继承 UIPercentDrivenInteractiveTransition 的,而这个UIPercentDrivenInteractiveTransition是遵守了
        // UIViewControllerInteractiveTransitioning 协议的,所以这里初始化返回这个是没有问题的
        
        // 是手势操作,就返回这个交互式控制器
        if (self.gestureRecognizer)
                
                return [[SwipTransitionInteractionController alloc] initWithGestureRecognizer:self.gestureRecognizer edgeForDragging:self.targetEdge];
        else
                return nil;
}

     

       看了上面的代码,我们的UIPercentDrivenInteractiveTransition就算出场了,SwipTransitionInteractionController是继承自UIPercentDrivenInteractiveTransition,你也知道UIPercentDrivenInteractiveTransition遵守了UIViewControllerInteractiveTransitioning协议,这里你也就应该理解我们初始化SwipTransitionInteractionController返回的意思了。

      接着看我们SwipTransitionInteractionController里面核心的代码:

/**
   前面代理通过 interactionControllerForPresentation 方法获取交互控制器的时候,手势返回的就是SwipTransitionInteractionController,这个时候就会调用这个方法
 
   interactive 交互
 */
-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
        
        [super startInteractiveTransition:transitionContext];
        self.transitionContext = transitionContext;
}


// 手势触发该方法
-(void)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer{
        
        switch (gestureRecognizer.state){
                        
                        
                case UIGestureRecognizerStateBegan:
                        
                        break;
                case UIGestureRecognizerStateChanged:
                        
                        // 调用updateInteractiveTransition来更新动画进度
                        // 里面嵌套定义 percentForGesture 方法计算动画进度
                        [self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
                        break;
                        
                case UIGestureRecognizerStateEnded:
                        
                        //判断手势位置,要大于一般,就完成这个转场,要小于一半就取消
                        if ([self percentForGesture:gestureRecognizer] >= 0.5f)
                                
                                // 完成交互转场
                                [self finishInteractiveTransition];
                        else
                                // 取消交互转场
                                [self cancelInteractiveTransition];
                        break;
                default:
                        
                        [self cancelInteractiveTransition];
                        break;
        }
}

// 计算动画进度
-(CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture{
      
        UIView * transitionContainerView = self.transitionContext.containerView;
        
        // 手势滑动 在transitionContainerView中 的位置
        // 这个位置判断的方法可以具体根据你的需求确定
        CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];
        
        CGFloat width  = CGRectGetWidth(transitionContainerView.bounds);
        CGFloat height = CGRectGetHeight(transitionContainerView.bounds);
        
        if (self.edge == UIRectEdgeRight)
                
                return (width - locationInSourceView.x) / width;
        else if (self.edge == UIRectEdgeLeft)
                
                return locationInSourceView.x / width;
        else if (self.edge == UIRectEdgeBottom)
                
                return (height - locationInSourceView.y) / height;
        else if (self.edge == UIRectEdgeTop)
                
                return locationInSourceView.y / height;
        else
                return 0.f;
}

       

上面的代码有几个点说一下:

      1、大家注意一下初始化的时候我们使用一个手势去接收传递到我们SwipTransitionInteractionController的手势,这也是下面手势事件能够执行的原因;

      2、这个startInteractiveTransition方法是我们UIViewControllerInteractiveTransitioning协议里面的方法,这个最主要的功能及时获取我们的交互上下文

           self.transitionContext = transitionContext 也就是这句代码

      3、再有一点就是我们的gestureRecognizeDidUpdate手势方法里面的更新进度以及取消和完成了,也就这几个地方大家需要注意点;

 

NOTE: 看看下面的打印日志

       fromView = nil

       但我们的fromViewController.View 确实是存在的,在上面的Demo中你可以看一下打印,确实是这样:以前看博客有同行说:

       UIView  * fromView   = [transitionContext viewForKey:UITransitionContextFromViewKey];

       UIView  * fromView   = fromViewController.View 是等价的,这样说应该是不成立的。

       然后在这里:TransitionAnimation 学习笔记 开头给出了答案,再理解一下。

 

      Demo的下载地址这里再发一次: 这里是Demo的下载地址

END:      

       由于篇幅的原因,剩下的几点要素我们在下一篇当中接着说,当然你看到这篇文章的时候,第二篇我也肯定是总结完了的。不然会看着断片的。就像前面说的那样,在第二篇当中多给一些实际的例子给大家参考。共同学习。  

    iOS 转场动画探究(二) 我们接着看..........      

posted @ 2017-06-26 15:38  MrRisingSun  阅读(6350)  评论(1编辑  收藏  举报