iOS-自定义转场动画

iOS中推出控制器的方式有两种:push和present,iOS的push动画基本上已经成为苹果的一个标志,最好不要自定义,不然和系统的动画不一样会显得不和谐。
关于present,更多的可参考:present和dismiss

下面介绍如何自定义present方式的转场动画。

1. UIViewControllerTransitioningDelegate协议

想自定义转场动画的VC必须遵守UIViewControllerTransitioningDelegate协议,实现协议的如下方法:

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [EOCPresentAnimator new];
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return [EOCDismissAnimator new];
}

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
    return  interactiveTransition;
}

解释:

  1. 方法1是present的界面添加动画,返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议。
  2. 方法2是为dismiss的界面添加动画,返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议。
  3. 方法3是控制转场进度的类,返回的对象要遵守UIViewControllerInteractiveTransitioning协议。
    系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议,我们直接使用它的子类。

2. 自定义动画类

接下来我们自定义动画类,遵守UIViewControllerAnimatedTransitioning协议,实现协议的两个方法,如下:

PresentAnimator.m文件:

#import "EOCPresentAnimator.h"

@implementation EOCPresentAnimator

//时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 2.f;
}

//动作 系统会自己调用
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    //上下文对象包含了全部信息
    //获取容器View
    UIView *containerView = transitionContext.containerView;
    //获取到toView:也就是说从ViewCtrlA跳转到ViewCtrlB,toView是ViewCtrlB.view
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    [containerView addSubview:toView];
    
    //rect范围的偏移量,大于0在右半边,下边
    CGRect frame = CGRectOffset(toView.frame, 0.f, [UIScreen mainScreen].bounds.size.height);
    toView.frame = frame;
    
    [UIView animateWithDuration:2.f animations:^{
        toView.frame = CGRectOffset(toView.frame, 0.f, -[UIScreen mainScreen].bounds.size.height);
    } completion:^(BOOL finished) {
        //结束上下文
        [transitionContext completeTransition:YES];
    }];
}
@end

DismissAnimator.m文件:

#import "EOCDismissAnimator.h"

@implementation EOCDismissAnimator

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 2.f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    UIView *containerView = transitionContext.containerView;
    
    //获取到toView:从ViewCtrlB dismiss 到ViewCtrlA   fromView是B, toView是A
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    
    CGRect finalFrame = CGRectOffset(fromView.frame, 0.f, [UIScreen mainScreen].bounds.size.height);
    
    //containerView里面有fromView了
    //把toView放到最下面
    [containerView insertSubview:toView atIndex:0];
    
    [UIView animateWithDuration:2.f animations:^{
        fromView.frame = finalFrame;
    } completion:^(BOOL finished) {
        //它肯定实现了移除fromView的操作  取消就不完成
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
        
        //自己移除fromView不行,如果不结束转场,transitionView还在
        //[fromView removeFromSuperview];
    }];
}
@end

解释:

  1. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;方法会在从一个VC跳转到另一个VC的时候,系统自动调用。
  2. 上个方法的参数transitionContext(转场上下文)是转场的中间人,里面保存了fromVC、toVC、containerView以及completeTransition:方法等信息。

动画类创建完成之后,我们在animationControllerForPresentedController:方法和animationControllerForDismissedController:方法里面传入两个动画对象,如下:

#pragma mark - UIViewControllerTransitioningDelegate
//为present的界面添加动画, 返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [EOCPresentAnimator new];
}

//为dismiss的界面添加动画, 返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return [EOCDismissAnimator new];
}

调用:

EOCNextViewController *nextViewCtrl = [[EOCNextViewController alloc] init];
nextViewCtrl.transitioningDelegate = self;
[self presentViewController:nextViewCtrl animated:YES completion:nil];

效果图:
present和dismiss.gif

下面有个新需求,如何在灰色界面,通过下滑手势dismiss到上一个界面,这里我们就需要用到interactionControllerForDismissal:方法了。

#pragma mark - UIViewControllerTransitioningDelegate
//控制转场进度的类, 返回的对象要遵守UIViewControllerInteractiveTransitioning协议
//系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议, 我们直接使用它的子类
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
    return  interactiveTransition;
}

这个方法需要返回一个遵守UIViewControllerInteractiveTransitioning协议的对象,由于系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议,我们直接使用它的子类,代码如下:

EOCInteractiveTransition.h文件

//  控制转场进度的类

#import <UIKit/UIKit.h>

@interface EOCInteractiveTransition : UIPercentDrivenInteractiveTransition

- (void)transitionToViewController:(UIViewController *)toViewController;

@end

EOCInteractiveTransition.m文件

#import "EOCInteractiveTransition.h"

@interface EOCInteractiveTransition () {
    UIViewController *presentedViewController;
    BOOL shouldComplete; //是否拖拽了一半以上
}

@end

@implementation EOCInteractiveTransition

- (void)transitionToViewController:(UIViewController *)toViewController {
    
    presentedViewController = toViewController;
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
    [toViewController.view addGestureRecognizer:panGesture];
}

- (void)panAction:(UIPanGestureRecognizer *)gesture {
    
    switch (gesture.state) {
        case UIGestureRecognizerStateBegan:
            
            [presentedViewController dismissViewControllerAnimated:YES completion:nil];
            
            break;
        case UIGestureRecognizerStateChanged: {
            
            //监听当前滑动的距离
            CGPoint transitionPoint = [gesture translationInView:presentedViewController.view];
            NSLog(@"transitionPoint %@", NSStringFromCGPoint(transitionPoint));
            
            CGFloat ratio = transitionPoint.y/[UIScreen mainScreen].bounds.size.height;
            NSLog(@"ratio: %f", ratio);
            
            if (ratio >= 0.5) {
                shouldComplete = YES;
            } else {
                shouldComplete = NO;
            }
            [self updateInteractiveTransition:ratio];
        }
            break;
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            if (shouldComplete) {
                [self finishInteractiveTransition];
            } else {
                [self cancelInteractiveTransition];
            }
        }
            break;
        default:
            break;
    }
}
@end

调用:

EOCNextViewController *nextViewCtrl = [[EOCNextViewController alloc] init];
[interactiveTransition transitionToViewController:nextViewCtrl];
nextViewCtrl.transitioningDelegate = self;
[self presentViewController:nextViewCtrl animated:YES completion:nil];

效果图:
下滑dismiss.gif

注意点:

  1. 没present的时候,层级结构如下
最开始.png
  1. present之后,层级结构如下
present之后.png

可以发现:
① present之后多了一个UITransitionView,这个View是专门做转场的。
② present之后并没有把控制器或者view直接盖上去,而是先移除旧的再添加新的,这个和push不一样。

自定义转场动画以及自定义容器动画Demo:自定义转场动画

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

推荐阅读更多精彩内容

  • iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能...
    iceMaple阅读 1,953评论 0 13
  • 路漫漫其修远兮,吾将上下而求索 前记 想研究自定义转场动画很久了,时间就像海绵,挤一挤还是有的,花了差不多有10天...
    半笑半醉間阅读 7,465评论 10 51
  • iOS7.0后苹果提供了自定义转场动画的API,利用这些API我们可以改变 push和pop(navigation...
    薛定喵的鹅阅读 17,749评论 1 37
  • 简书上的所有内容都可以在我的个人博客上找到 这两天学习了一下自定义转场动画的内容,刚开始看的时候被这几个又长又很相...
    yahtzee_阅读 1,268评论 1 13
  • 泰国之行的最后一个晚上,躺在床上,感慨不多,只觉得疲累,肌肉酸痛,心路坎坷。想到被人占便宜,心里堵的不行。看到赵老...
    发财小姐阅读 303评论 0 0