iOS自定义转场动画/UIPresentationController

一.UIPresentationController简介

  • UIPresentationController是 iOS8 新增的一个API,苹果的官方定义是:对象为所呈现的视图控制器提供高级视图的转换管理(从呈现视图控制器的时间直到它被消除期间)。其实说白了就是用来控制controller之间的跳转特效。比如希望实现一个特效,显示一个窗口,大小和位置都是自定义的,并且遮罩在原来的页面上。
  • 下图是苹果官方自定义modal动画的栗子,点我直接下载
苹果官方Demo.gif
  • 网上找到的UIPresentationController在自定义modal动画中的位置:
    UIPresentationController作用.png

二.UIPresentationController作用

  • 管理所有Modal出来的控制器
  • 管理\监听 切换控制器的过程
  • 控制器一旦调了presentViewController方法,控制器的presentationController,会先创建好了,然后整个控制器的切换由presentationController管理。如下代码:
AViewController *vc = [[AViewController alloc] init];
[self presentViewController:vc animated:YES completion:nil];

三.UIPresentationController方法和属性介绍

/**
 构造方法,苹果建议使用这个初始化UIPresentationController
 @param presentedViewController 将要跳转到的目标控制器
 @param presentingViewController 跳转前的原控制器
 */
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController;

presentedViewController:     要 modal 显示的视图控制器
presentingViewController:    跳转前视图控制器
containerView()              容器视图
presentedView()              被展现控制器的视图

func presentationTransitionWillBegin()          跳转将要开始
func presentationTransitionDidEnd(completed: Bool)  跳转完成
func dismissalTransitionWillBegin()         dismiss将要开始
func dismissalTransitionDidEnd(completed: Bool)     dismiss完成
func frameOfPresentedViewInContainerView()    动画之后,目标控制器View的位置

四.自定义modal转场动画的第三个步骤

  • 第一步.写一个遵守UIViewControllerTransitioningDelegate协议的类,来告诉控制器,谁是动画主管(UIPresentationController),谁是开始动画的具体细节负责类、谁是结束动画的具体细节负责类。

  • 第二步.写一个UIPresentationController的子类(动画主管) --> 负责「被呈现」及「负责呈现」的controller以外的controller,比如带渐变效果的黑色半透明背景View。在此步骤,起码需要重写以下5个方法:

    • 1.presentationTransitionWillBegin
    • 2.presentationTransitionDidEnd:
    • 3.dismissalTransitionWillBegin
    • 4.dismissalTransitionDidEnd:
    • 5.frameOfPresentedViewInContainerView
  • 第三步.写一个遵守UIViewControllerAnimatedTransitioning协议的类,负责动画细节。比如怎么出现,位置在哪,动画细节如何等。

  • 下面将通过几个Demo,来实现自定义转场动画,建议直接下载Demo运行看效果,gif图有点失真失帧。

  • Demo下载地址: 点我下载

    Demo效果图.gif

五.自定义modal转场动画第一个Demo

  • 第一个Demo比较简单,不用我们自己去写动画效果,这里将贴出部分代码,后续Demo将不贴代码了,可以自己去下载Demo看看,里面加了很详细的注释。
  • 按照上面所写的步骤,为了方便,我们将第一步、第二步合并在一起,使用一个类同时实现
    UIViewControllerTransitioningDelegate协议,并继承UIPresentationController
  • h文件的内容:
#import <UIKit/UIKit.h>
/**
 * 实现自定义过渡动画:
 * 1.继承UIPresentationController 成为子类
 * 2.遵守UIViewControllerAnimatedTransitioning 协议
 * 其实也可以写成两个类,分别继承UIPresentationController和实现UIViewControllerAnimatedTransitioning协议
 */
@interface BCustomPresentationController : UIPresentationController <UIViewControllerTransitioningDelegate>
/** 黑色半透明背景 */
@property (nonatomic, strong) UIView *dimmingView;
@end
  • m文件的内容:
@implementation BCustomPresentationController

//| ------------------------------第一步内容----------------------------------------------
#pragma mark - UIViewControllerTransitioningDelegate
/*
 * 来告诉控制器,谁是动画主管(UIPresentationController),因为此类继承了UIPresentationController,就返回了self
 */
- (UIPresentationController* )presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
    return self;
}

//| ------------------------------第二步内容----------------------------------------------
#pragma mark - 重写UIPresentationController个别方法
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController {
    self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
    if (self) {
        // 必须设置 presentedViewController 的 modalPresentationStyle
        // 在自定义动画效果的情况下,苹果强烈建议设置为 UIModalPresentationCustom
        presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
    }
    return self;
}

// 呈现过渡即将开始的时候被调用的
// 可以在此方法创建和设置自定义动画所需的view
- (void)presentationTransitionWillBegin {
    // 背景遮罩
    UIView *dimmingView = [[UIView alloc] initWithFrame:self.containerView.bounds];
    dimmingView.backgroundColor = [UIColor blackColor];
    dimmingView.opaque = NO; //是否透明
    dimmingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [dimmingView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dimmingViewTapped:)]];
    self.dimmingView = dimmingView;
    
    [self.containerView addSubview:dimmingView]; // 添加到动画容器View中。
    
    // 获取presentingViewController 的转换协调器,应该动画期间的一个类?上下文?之类的,负责动画的一个东西
    id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
    
    // 动画期间,背景View的动画方式
    self.dimmingView.alpha = 0.f;
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.dimmingView.alpha = 0.4f;
    } completion:NULL];
}

#pragma mark 点击了背景遮罩view
- (void)dimmingViewTapped:(UITapGestureRecognizer*)sender {
    [self.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
}

// 在呈现过渡结束时被调用的,并且该方法提供一个布尔变量来判断过渡效果是否完成
- (void)presentationTransitionDidEnd:(BOOL)completed {
    // 在取消动画的情况下,可能为NO,这种情况下,应该取消视图的引用,防止视图没有释放
    if (!completed) {
        self.dimmingView = nil;
    }
}

// 消失过渡即将开始的时候被调用的
- (void)dismissalTransitionWillBegin {
    id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
    
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.dimmingView.alpha = 0.f;
    } completion:NULL];
}

// 消失过渡完成之后调用,此时应该将视图移除,防止强引用
- (void)dismissalTransitionDidEnd:(BOOL)completed {
    if (completed == YES) {
        [self.dimmingView removeFromSuperview];
        self.dimmingView = nil;
    }
}

// 返回目标控制器Viewframe
- (CGRect)frameOfPresentedViewInContainerView {
    // 这里直接按照想要的大小写死,其实这样写不好,在第二个Demo里,我们将按照苹果官方Demo,写灵活的获取方式。
    CGFloat height = 300.f;
    
    CGRect containerViewBounds = self.containerView.bounds;
    containerViewBounds.origin.y = containerViewBounds.size.height - height;
    containerViewBounds.size.height = height;
    return containerViewBounds;
}

//  建议就这样重写就行,这个应该是控制器内容大小变化时,就会调用这个方法, 比如适配横竖屏幕时,翻转屏幕时
//  可以使用UIContentContainer的方法来调整任何子视图控制器的大小或位置。
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
    [super preferredContentSizeDidChangeForChildContentContainer:container];
    if (container == self.presentedViewController) [self.containerView setNeedsLayout];
}

- (void)containerViewWillLayoutSubviews {
    [super containerViewWillLayoutSubviews];
    self.dimmingView.frame = self.containerView.bounds;
}
@end
  • 疑问:为什么Demo1里没有按照三个步骤里指定 动画的具体细节实现类,也有动画效果呢?
  • 因为在 弹出控制器时,指定了animated为YES,那么此时如果没有实现UIViewControllerAnimatedTransitioning动画细节类,此时就会使用系统的presentViewController动画效果,即:从下往上弹出,从上往下消失。

六.自定义modal转场动画第二个Demo

  • 此Demo2在Demo1的基础上,实现了 UIViewControllerAnimatedTransitioning协议,实现了对动画效果的定制,UIViewControllerAnimatedTransitioning协议主要实现以下几个方法:
/| ------------------------------第三步内容----------------------------------------------
#pragma mark UIViewControllerAnimatedTransitioning具体动画实现
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    // 动画时长
    return 0.45 ;
}

// 核心,动画效果的实现
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    // 1.获取源控制器、目标控制器、动画容器View
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    __unused UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    UIView *containerView = transitionContext.containerView;
    
    // 2. 获取源控制器、目标控制器 的View,但是注意二者在开始动画,消失动画,身份是不一样的:
    // 也可以直接通过上面获取控制器获取,比如:toViewController.view
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    
    [containerView addSubview:toView];  //必须添加到动画容器View上。

    // 3.设置动画具体细节,使用[UIView animate...]动画,或者其他方式呈现动画。

    // 4.动画结束时,必须执行下句代码
    [transitionContext completeTransition:YES];    
}

- (void)animationEnded:(BOOL) transitionCompleted {
    // 动画结束...
}
  • 在Demo1里,我们在 UIPresentationController 里写死了目标控制器View的位置大小,从面向对象的角度讲,是不合理的,目标控制器View的大小,它自己最清楚,所在我们在Demo2里,仿照苹果官方Demo,完善了这一个问题。(其实如果在上面UIViewControllerAnimatedTransitioning协议里实现了动画前后的位置和大小,那么在UIPresentationController可以不再重写sizeframe相关的几个方法)

七.自定义modal转场动画第三个Demo

  • 此效果的实现思路,灵感是从控制器modalPresentationStyle属性值上来的,当modalPresentationStyleUIModalPresentationCustom的时候,就presentViewController目标控制器之后,把目标控制器的view设置为透明,就能看到源控制器。
toVC.modalPresentationStyle = UIModalPresentationCustom ; //必须是UIModalPresentationCustom
toVC.view.backgroundColor = [UIColor clearColor];  //必须是clearColor
[self presentViewController:toVC animated:NO completion:nil]; //animated:必须是NO
  • 基于以上对目标控制的设置之后,就可以在目标控制器的 viewWillAppear:方法里自定义动画效果。核心代码如下:(建议下载Demo查看详细注释)
#pragma mark - 在此方法做动画呈现
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    self.bgView.frame = [UIScreen mainScreen].bounds;
    self.contentView.frame = CGRectMake(0,[UIScreen mainScreen].bounds.size.height, CGRectGetWidth([UIScreen mainScreen].bounds), 300.0);
    
    [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.95 initialSpringVelocity:0.05 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        
        self.bgView.alpha = 1.0f ;
        self.contentView.frame = CGRectMake(0, [UIScreen mainScreen].bounds.size.height - 300.f, CGRectGetWidth([UIScreen mainScreen].bounds), 300.0f);
        
    } completion:^(BOOL finished) {
    }];
}

#pragma mark - 消失
- (void)clickBgView {
    [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.95 initialSpringVelocity:0.05 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        
        self.bgView.alpha = 0.0f ;
        self.contentView.frame = CGRectMake(0,[UIScreen mainScreen].bounds.size.height, CGRectGetWidth([UIScreen mainScreen].bounds), 300.0);
        
    } completion:^(BOOL finished) {
        // 动画Animated必须是NO,不然消失之后,会有0.35s时间,再点击无效
        [self dismissViewControllerAnimated:NO completion:nil];
    }];
}

END。
我是小侯爷。
在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
如果读完觉得有收获的话,记得关注和点赞哦。
非要打赏的话,我也是不会拒绝的。

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

推荐阅读更多精彩内容