自定义转场动画-push动画

准备:

苹果在iOS7之后,提供了自定义转场API。使得我们可以对模态(present、dismiss)、导航控制器(push、pop)、标签控制器的切换进行自定义转场。前些天在项目空档期,仿做了小红书,用到了这个效果,所以今天,就以实战为基础做讲解。具体详细的介绍请参考:唐巧-iOS 视图控制器转场详解喵神-WWDC 2013 Session笔记 - iOS7中的ViewController切换

效果:

小红书push转场动画

是不是感觉仿的很逼真啊!哈哈哈。请允许我嘚瑟一下。好了下面进入正题。

下面先介绍几个重要的协议:
UIViewControllerContextTransitioning
这个接口用来提供切换上下文给开发者使用,包含了从哪个VC到哪个VC等各类信息,一般不需要开发者自己实现。具体来说,iOS7的自定义切换目的之一就是切换相关代码解耦,在进行VC切换时,做切换效果实现的时候必须要需要切换前后VC的一些信息,系统在新加入的API的比较的地方都会提供一个实现了该接口的对象,以供我们使用。
对于切换的动画实现来说(这里先介绍简单的动画,在后面我会再引入手势驱动的动画),这个接口中最重要的方法有:

  • -(UIView *)containerView; VC切换所发生的view容器,开发者应该将切出的view移除,将切入的view加入到该view容器中。
  • -(UIViewController *)viewControllerForKey:(NSString *)key; 提供一个key,返回对应的VC。现在的SDK中key的选择只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey两种,分别表示将要切出和切入的VC。
  • -(CGRect)initialFrameForViewController:(UIViewController *)vc; 某个VC的初始位置,可以用来做动画的计算。
  • -(CGRect)finalFrameForViewController:(UIViewController *)vc; 与上面的方法对应,得到切换结束时某个VC应在的frame。
  • -(void)completeTransition:(BOOL)didComplete; 向这个context报告切换已经完成。

UIViewControllerAnimatedTransitioning
这个接口负责切换的具体内容,也即“切换中应该发生什么”。开发者在做自定义切换效果时大部分代码会是用来实现这个接口。它只有两个方法需要我们实现:

  • -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning >)transitionContext; 系统给出一个切换上下文,我们根据上下文环境返回这个切换所需要的花费时间(一般就返回动画的时间就好了,SDK会用这个时间来在百分比驱动的切换中进行帧的计算,后面再详细展开)。
  • -(void)animateTransition:(id<UIViewControllerContextTransitioning >)transitionContext; 在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。

UIViewControllerTransitioningDelegate
这个接口的作用比较简单单一,在需要VC切换的时候系统会像实现了这个接口的对象询问是否需要使用自定义的切换效果。这个接口共有四个类似的方法:

  • -(id<UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController)presented presentingController:(UIViewController)presenting sourceController:(UIViewController*)source;
  • -(id<UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController *)dismissed;
  • -(id<UIViewControllerInteractiveTransitioning >)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator;
  • -(id<UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator;
    前两个方法是针对动画切换的,我们需要分别在呈现VC和解散VC时,给出一个实现了UIViewControllerAnimatedTransitioning接口的对象(其中包含切换时长和如何切换)。后两个方法涉及交互式切换,之后再说。

了解了上面的协议代理之后,咱们正式开始:

  1. 首先我们要自定义一个遵循<UIViewControllerAnimatedTransitioning>协议的动画过渡管理对象,实现两个必要方法:
//返回动画事件  
 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
 //所有的过渡动画事务都在这个方法里面完成
 - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
  1. 另外根据需求我们也可以自定义一个继承UIPercentDrivenInteractiveTransition的手势过渡管理对象。使我们可以通过手势触发转场动画。如滑动屏幕左侧,pop到上一页。
  2. 我们今天要做的是导航控制器动画,所以主要实现下面两个代理方法
//返回转场动画过渡管理对象
 - (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                   interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
 //返回手势过渡管理对象
 - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                         fromViewController:(UIViewController *)fromVC
                                           toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

直接上关键代码了:

  1. 首先创建两个需要跳转的容器。(HomeViewController)VC1,(NotesDetailViewController)VC2.
  • HomeViewController*的关键代码
    首先要遵循:UINavigationControllerDelegate
-(NavTransitioning *)pushTransition
{
    if (!_pushTransition) {
        _pushTransition = [[NavTransitioning alloc] init];
    }
    
    return _pushTransition;
}
#pragma mark UINavigationControllerDelegate
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    if (operation == UINavigationControllerOperationPush && [toVC isKindOfClass:[NotesDetailViewController class]]) {
        return self.pushTransition;
    }else{
        return nil;
    }
}
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
    return self.interactionController;
}  - (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.navigationController.delegate = self;
}  - (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if (self.navigationController.delegate == self) {
        self.navigationController.delegate = nil;
    }
}
  • NotesDetailViewController*的关键代码
    首先要遵循:UINavigationControllerDelegate
#pragma mark - <UINavigationControllerDelegate>
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController
{
    if ([animationController isKindOfClass:[NavTransitioningBack class]]) {
        return _interactivePopTransition;
    }else{
        return nil;
    }
}
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
    if ([toVC isKindOfClass:[HomeViewController class]])
{
        return self.backTransition;
    }else{
        return nil;
    }
}-(NavTransitioningBack *)backTransition
{
    if (!_backTransition) {
        _backTransition = [[NavTransitioningBack alloc]init];
    }
    return _backTransition;
}
-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
}
-(void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if (self.navigationController.delegate == self) {
        self.navigationController.delegate = nil;
    }
}

为该页面添加手势:

    UIScreenEdgePanGestureRecognizer *popRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopRecognizer:)];
    popRecognizer.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:popRecognizer];
-(void)handlePopRecognizer:(UIScreenEdgePanGestureRecognizer *)recognizer
{
    CGFloat progress = [recognizer translationInView:self.view].x / self.view.bounds.size.width;
    progress = MIN(1.0, MAX(0.0, progress));
    
    if (recognizer.state == UIGestureRecognizerStateBegan) {
        self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self.navigationController popViewControllerAnimated:YES];
        
    }else if (recognizer.state == UIGestureRecognizerStateChanged){
        [self.interactivePopTransition updateInteractiveTransition:progress];
    }else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled){
        if (progress > 0.5) {
            [self.interactivePopTransition finishInteractiveTransition];
        }else{
            [self.interactivePopTransition cancelInteractiveTransition];
        }
        
        self.interactivePopTransition = nil;
    }
}
  1. 下面开始创建遵循NavTransitioningUIViewControllerAnimatedTransitioning的动画过渡管理类。
/**
 *  这个接口负责切换的具体内容,也即“切换中应该发生什么”。开发者在做自定义切换效果时大部分代码会是用来实现这个接口
 */
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0.6f;
}
/**
 *  UIViewControllerAnimatedTransitioning 的协议都包含一个对象:transitionContext,通过这个对象能获取到切换时的上下文信息,比如从哪个VC切换到哪个VC等。我们从 transitionContext 获取 containerView,这是一个特殊的容器,切换时的动画将在这个容器中进行;UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey 就是从哪个VC切换到哪个VC
 */-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //通过viewControllerForKey取出转场前后的两个控制器
    HomeViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    NotesDetailViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    //这里有个重要的概念containerView,如果要对视图做转场动画,视图就必须要加入containerView中才能进行,可以理解containerView管理着所有做转场动画的视图
    UIView *containerView = [transitionContext containerView];
    
    fromVC.currentIndexPath = [[fromVC.collectionView indexPathsForSelectedItems] firstObject];
    
    NotesCollectionCell *cell = (NotesCollectionCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.currentIndexPath];
    //snapshotViewAfterScreenUpdates 获取快照 对cell的imageView截图保存成另一个视图用于过渡,并将视图转换到当前控制器的坐标
    UIView *snapShotView = [cell.itemImage snapshotViewAfterScreenUpdates:NO];
    //坐标转换
    snapShotView.frame = fromVC.finalCellRect = [containerView convertRect:cell.itemImage.frame fromView:cell.itemImage.superview];
    
    cell.itemImage.hidden = YES;
    //设置toVC的frame
    toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
    toVC.view.alpha = 0;
    toVC.imageScrollView.hidden = YES;
    fromVC.view.alpha = 0;
    
    [containerView addSubview:toVC.view];
    [containerView addSubview:snapShotView];
//转场过程中要执行的动画
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.0f initialSpringVelocity:0.0f options:UIViewAnimationOptionCurveLinear animations:^{
        [containerView layoutIfNeeded];
        toVC.view.alpha = 1.0;
        snapShotView.frame = [containerView convertRect:toVC.imageScrollView.frame fromView:toVC.imageScrollView.superview];
    } completion:^(BOOL finished) {
        toVC.imageScrollView.hidden = NO;
        fromVC.view.alpha = 1;
        cell.itemImage.hidden = NO;
        [snapShotView removeFromSuperview];
        //使用如下代码标记整个转场过程是否正常完成[transitionContext transitionWasCancelled]代表手势是否取消了,如果取消了就传NO表示转场失败,反之亦然,如果不用手势的话直接传YES也是可以的,但是无论如何我们都必须标记转场的状态,系统才知道处理转场后的操作,否者认为你一直还在转场中,会出现无法交互的情况,切记!
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }];
}
  1. 下面开始创建遵循NavTransitioningBackUIViewControllerAnimatedTransitioning的动画过渡管理类。
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.6f;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
    NotesDetailViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    HomeViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    UIView *containerView = [transitionContext containerView];
    
    UIView *snapShotView = [fromVC.imageScrollView snapshotViewAfterScreenUpdates:NO];
    snapShotView.frame = [containerView convertRect:fromVC.imageScrollView.frame fromView:fromVC.imageScrollView.superview];
    fromVC.imageScrollView.hidden = YES;
    NSLog(@"********1 %@",NSStringFromCGRect(toVC.view.frame));
    toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
    toVC.view.alpha = 0;
    
    NotesCollectionCell *cell = (NotesCollectionCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.currentIndexPath];
    cell.itemImage.hidden = YES;
    
    [containerView addSubview:toVC.view];
    [containerView addSubview:snapShotView];
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        toVC.view.alpha = 1.0;
        snapShotView.frame = toVC.finalCellRect;
        
    }completion:^(BOOL finished) {
        [snapShotView removeFromSuperview];
        fromVC.imageScrollView.hidden = NO;
        cell.itemImage.hidden = NO;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

至此,结束。做的可能不是太完美,如果有什么问题,随便提,共同提高。谢谢!

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

推荐阅读更多精彩内容