iOS之实现自定义转场动画

转场/控制器切换

含义:在 NavigationControllerpushpop 一个 View Controller,在TabBarController中切换到其他 View Controller,以 Modal方式显示另外一个View Controller,这些都是控制器切换(View Controller Transition),简称转场

iOS 7 之前,我们只能使用系统提供的转场效果,iOS 7之后苹果开放了相关 API 允许我们对转场效果进行全面定制,它是 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活,这样对自定义转场动画以及交互手段的支持带来了无限可能。

转场实现的本质

转场的本质是当前视图消失和下一视图出现,基于此进行动画,因为在转场的过程中,作为容器的父控制器维护着多个子控制器,但在视图结构上,只保留一个子控制器的视图。

目前主流的自定义转场

  1. UINavigationControllerPush 和 Pop
  1. UITabBarController中切换 TabBar
  2. 模态(Modal) 转场:Present 和 Dismiss,仅限于modalPresentationStyle属性为 UIModalPresentationFullScreenUIModalPresentationCustom这两种模式
  3. UICollectionViewController 布局转场(与 UINavigationController结合的转场方式)

转场动画的实现

转场协议由5种协议组成,在实际开发中只需我们提供其中的两个或三个便能实现绝大部分的转场动画。

  • 1.转场代理(Transition Delegate) - 必须使用:
    自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场,不同类型的控制器遵守的协议不同,如下:
/**
 *  除了<UIViewControllerTransitioningDelegate>是 iOS7 新增的协议,其他两种在 iOS2 里就存在了
 */
<UINavigationControllerDelegate> //UINavigationController 的 delegate 属性遵守该协议
<UITabBarControllerDelegate> //UITabBarController 的 delegate 属性遵守该协议
<UIViewControllerTransitioningDelegate> //UIViewController 的 transitioningDelegate 属性遵守该协议
  • 2.动画控制器(Animation Controller) - 必须使用:
    最重要的部分,遵守<UIViewControllerAnimatedTransitioning>协议,负责添加视图以及执行动画,由我们实现
  • 3.交互控制器(Interaction Controller) - 可选使用:
    通过交互手段,通常是手势来驱动动画控制器实现的动画,使得用户能够控制整个过程;遵守<UIViewControllerInteractiveTransitioning>协议
  • 4.转场环境(Transition Context) - 必须使用:
    提供转场中需要的数据;遵守<UIViewControllerContextTransitioning>协议;由UIKit转场开始前生成并提供给我们提交的动画控制器和交互控制器使用
  • 5.转场协调器(Transition Coordinator) - 可选使用:
    可在转场动画发生的同时并行执行其他的动画,主要在 Modal 转场和交互转场取消时使用,其他时候很少用到,由 UIKit 在转场时生成,遵守<UIViewControllerTransitionCoordinator>协议;UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一个遵守该协议的对象,且该方法只在该控制器处于转场过程中才返回一个此类对象,不参与转场时返回 nil

下面就拿一个我之前写好的小栗子来看看自定义转场是如何实现的吧,这里我以特殊的Modal转场为例讲一下,TabBarController或是NavigationController自定义转场实现相对简单些,但本质其实都是相同的,先看效果图吧:

Modal转场

Modal转场的实现

  • Demo内的视图结构
视图结构.png

首先控制器遵守转场代理

//实现转场的触发操作
- (IBAction)clickBtn {
    PresentedViewController *presentedVC = [PresentedViewController new];
    
    //系统转场
    //presentedVC.modalTransitionStyle = UIModalTransitionStyleCoverVertical; //默认样式
    
    //自定义转场 模态转场 需要代理实现
    presentedVC.modalPresentationStyle = UIModalPresentationCustom;
    
    //遵守转场代理  代理需要强引用(即:self.delegate)
    presentedVC.transitioningDelegate = self.delegate;
    
    [self presentViewController:presentedVC animated:YES completion:nil];
}

在TransitionDelegate遵守对应的协议

@interface TransitionDelegate : NSObject<UIViewControllerTransitioningDelegate>

实现其代理协议


/**
 *  返回当view显示时执行动画的对象,该对象需实现转场动画
 */
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
    //return nil;
    
    return [ModalAnimation new];
}

/**
 *  返回当view消失时执行动画的对象,该对象需实现转场动画
 */
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
    //return nil;
    
    return [ModalAnimation new];
}

创建动画控制器添加转场视图以及执行相应的动画,对于动画控制器来说,转场方式并不重要,可以对fromView 和 toView 进行任何动画,需要遵守转场动画协议<UIViewControllerAnimatedTransitioning>,并实现对应的协议方法,如下:

//返回转场动画时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
    return 0.5;
}

/**
 *  最重要的方法 必须实现 否则自定义转场无效
 *  该方法接受一个遵守<UIViewControllerContextTransitioning>协议的转场环境对象
 *  该转场环境对象提供了转场所需要的重要数据:参与转场的视图控制器和转场过程的状态信息
 *  在转场开始前<UIKit>生成遵守转场环境协议<UIViewControllerContextTransitioning>的对象transitionContext
 *  转场环境对象transitionContext提供了以下信息
 *  1. containerView 容器视图
 *  2. 获取参与转场的视图控制器,有 UITransitionContextFromViewControllerKey 和 UITransitionContextToViewControllerKey 两个 Key
 *  2. iOS8之后 新增的 API 用于方便获取参与转场的视图,有 UITransitionContextFromViewKey 和 UITransitionContextToViewKey 两个 Key
 *  在 iOS8 中可通过方法"viewForKey"来获取参与转场的三个重要视图,在 iOS7 中则需要通过对应的视图控制器来获取
 */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    
    //获取转场相关的两个控制器 iOS7的API
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    //iOS8之后使用
    //UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    //UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    
    //获取参与转场的视图
    UIView *fromView = fromVC.view;
    UIView *toView = toVC.view;
    
    //获取容器视图
    UIView *containerView = [transitionContext containerView];
    
    if (toVC.isBeingPresented) {
        //添加目标View
        [containerView addSubview:toView];
        
        //实现动画
        toView.transform = CGAffineTransformMakeRotation(-M_PI_2); //设置初始值
        
        //获取动画的时间
        //NSTimeInterval duration = [self transitionDuration:transitionContext];
        
        //回到默认位置
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toView.transform = CGAffineTransformIdentity;
        } completion:^(BOOL finished) {
            
            /**
             *  正确地结束转场过程。转场的结果有两种:完成或取消
             *  转场动画结束后 必须调用 否则呈现的新视图无法监听任何事件
             *  非交互转场的结果只有完成一种情况,不过交互式转场需要考虑取消的情况
             *  如何结束取决于转场的进度,通过transitionWasCancelled()方法来获取转场的状态,使用completeTransition:来完成或取消转场
             */
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }else{
        //视图消失 Dismiss 转场中不要将 toView 添加到 containerView
        if (fromVC.isBeingDismissed) {
            [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
                fromView.transform = fromView.transform.b > 0.20 ? CGAffineTransformMakeRotation(M_PI_2):CGAffineTransformMakeRotation(-M_PI_2);
            } completion:^(BOOL finished) {
                [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            }];
        }
    }
}

下面需要处理Modal转场后的控制器,因为Demo实现的一个视图基于某个点旋转,所以这里使用了layer的属性锚点anchorPoint,对于橘色视图的旋转监听,这里采用了拖动手势UIPanGestureRecognizer,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /**
     *  自定义转场 这里需要设置控制器View的大小
     *  anchorPoint 锚点 默认是其中心点(0.5,0.5) 设置必须在frame之前
     */
    self.view.backgroundColor = [UIColor orangeColor];
    self.view.layer.anchorPoint = CGPointMake(0.5, 2.0);
    self.view.frame = [UIScreen mainScreen].bounds;
    
    //创建拖动手势
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panView:)];
    
    [self.view addGestureRecognizer:pan];
}

处理拖动手势事件

- (void)panView:(UIPanGestureRecognizer *)pan{
    switch (pan.state) {
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:{
            
            NSLog(@"%f...",self.view.transform.b);
            
            /**
             *  根据矩阵中b的值大小操作橘色View  大于0.20实现让其掉落下来的效果 弧度为正代表顺时针
             *  CGAffineTransform的各种操作 旋转 平移 缩放 本质都是矩阵形式实现
             */
            if (ABS(self.view.transform.b) > 0.20) {
                [UIView animateWithDuration:1.0 animations:^{
                    self.view.transform =  self.view.transform.b >0 ? CGAffineTransformMakeRotation(M_PI_2): CGAffineTransformMakeRotation(-M_PI_2);
                }];
                
                [self dismissViewControllerAnimated:YES completion:nil]; //保证视图完全移除
            }else{
                self.view.transform = CGAffineTransformIdentity; //默认位置
            }
        }
            break;
        default:{
            //获取偏移量
            CGFloat offSetX = [pan translationInView:self.view].x;
            
            //计算偏移百分比
            CGFloat percentage = offSetX /self.view.bounds.size.width;
            
            //计算旋转的度数 这里设置旋转的度数范围 M_PI_2
            CGFloat radios = percentage * M_PI_2;
            
            //实现旋转
            self.view.transform = CGAffineTransformMakeRotation(radios);
        }
            break;
    }
}

到这里的话,Modal转场的核心基本知识就说完了,嗯…上述步骤不错的话,就能实现一个简单的自定义转场动画,其实对于自定义的转场动画,自定义容器的控制器转场应该是复杂度最高的,这里呢,先说下转场动画的简单使用吧,后面文章会更新下这个复杂知识点的,有什么理解错误的地方欢迎指正,谢谢咯。。。

附上Demo链接
参考的博文链接:http://blog.devtang.com/2016/03/13/iOS-transition-guide/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容