有关UINavigationController页面之间的跳转动画

最近遇到一个需求,一个导航控制器上有一个按钮点击弹出一个全屏幕的蒙层,然后蒙层上有个按钮点击从屏幕右边push过来一个页面;

先来张草图感受下需求是什么(很直观有木有):

页面跳转.png

我们可以形象的举个,如果蒙层我们也算做一个控制器的话,有三个控制器,oneVC,twoVC,threeVC,oneVC和threeVC是有导航栏的,twoVC(蒙层)是全屏幕的控制器,需求是点击oneVC上的按钮弹出twoVC(要求动画是弹出),点击twoVC上的按钮push过来threeVC(动画是一般的屏幕右边push过来)

好了,拿到这个需求,这不是很简单吗?刚开始做的时候也没多想,项目里有一种蒙层控制器弹出的方法,是UIViewController的分类方法,直接通过oneVC控制器调用presentInWindow

- (void)presentInWindow
{
    self.view.alpha = 0;
    self.view.backgroundColor = [UIColor clearColor];
    [self.view setTranslatesAutoresizingMaskIntoConstraints:NO];
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    [keyWindow addSubview:self.view];
    [self retain];
    
    NSDictionary *dic = @{@"_view_": self.view};
    [keyWindow addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_view_]|"
                                                                      options:0
                                                                      metrics:0
                                                                        views:dic]];
    [keyWindow addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_view_]|"
                                                                      options:0
                                                                      metrics:0
                                                                        views:dic]];
    
    __block typeof(self) weakSelf = self;
    
    [UIView animateWithDuration:0.3 animations:^{
        weakSelf.view.alpha = 1.0f;
    }];
}

- (void)dismiss
{
    __block typeof(self) weakSelf = self;
    
    [UIView animateWithDuration:0.3 animations:^{
        weakSelf.view.alpha = 0.0f;
    } completion:^(BOOL finished) {
        [weakSelf.view removeFromSuperview];
        [weakSelf release];
    }];
}

然后再点击twoVC上的按钮调用系统的push方法

[self.navigationController pushViewController:threeVC animated:YES];

好了 点击完根本没有任何反应,而且断点直接走到了threeVC的 -(void)dealloc方法,突然懵逼了,这才发现调用presentInWindow出来的twoVC并不是导航控制器,再调用push方法,所以跳转不过去;看到有人实现的一种方案,他是这样实现的:
首先调用presentInWindow弹出twoVC,然后点击twoVC上的按钮时,调用了 - (void)dismiss方法关闭twoVC,通过回调的方法从oneVC push到threeVC,但是这个有个问题就是返回的时候twoVC不见了,可能有人想问,我通过回调从 oneVC 跳转到threeVC的时候不关闭twoVC不就保证三个页面都在了么,再仔细看看才发现presentInWindow是把twoVC加到了UIWindow上,这样threeVC push过来的时候层级就在twoVC下面,就被盖着了。

经过尝试,找到了三种解决方案,分享给大家,实现方式都很简单,都是把三个VC作为导航控制器来跳转。

方案一

这种方式借助上面的presentInWindow方法,直接把twoVC包装成UINavigationController,然后去调用presentInWindow,代码如下:

    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:twoVC];
    [nav presentInWindow];

关闭twoVC调用

    [self.navigationController dismiss];

然后再跳转threeVC

    [self.navigationController pushViewController:threeVC animated:NO];

方案二

这种方式是利用系统的CATransition动画来设置弹出效果,废话不多说,上代码:

    CATransition *animation = [CATransition animation];
    [animation setDuration:0.3];
    [animation setType: kCATransitionFade];
    [animation setSubtype: kCATransitionFromTop];
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
    [self.navigationController pushViewController:twoVC animated:NO];
    [self.navigationController.view.layer addAnimation:animation forKey:nil];

关闭当前页面直接调用

      [self.navigationController popViewControllerAnimated:NO];

这种方式其实就是正常的push,只是给push添加了一个动画,也比较简单

方案三

这种方式相比前两种最为复杂,因为可以自定义,是iOS7新增的;
苹果给我们开发者提供的是都是协议接口,所以我们能够很好的单独提出来写成一个个类,在里面实现我们各种自定义效果
1.先来看看实现UIViewControllerAnimatedTransitioning的自定义动画类
我们定义了一个实体类CustomAnimator:

/**
 *  自定义的动画类
 *  实现协议------>@protocol UIViewControllerAnimatedTransitioning
 *  这个接口负责切换的具体内容,也即“切换中应该发生什么”
 */
//CustomAnimator.h
#import <Foundation/Foundation.h>
typedef enum {
    AnimationTypePresent,
    AnimationTypeDismiss
} AnimationType;

@interface CustomAnimator : NSObject<UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) AnimationType type;
@end

//CustomAnimator.m
@implementation CustomAnimator

// 系统给出一个切换上下文,我们根据上下文环境返回这个切换所需要的花费时间
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 1.0;
}

// 完成容器转场动画的主要方法,我们对于切换时的UIView的设置和动画都在这个方法中完成
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 可以看做为destination ViewController
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // 可以看做为source ViewController
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    if (self.type == AnimationTypePresent) {
        //Add 'to' view to the hierarchy with 0.0 scale
        toViewController.view.transform = CGAffineTransformMakeScale(0.95, 0.95);
        toViewController.view.alpha = 0;
        [containerView insertSubview:toViewController.view aboveSubview:fromViewController.view];
        
        //iOS9 兼容   
        [toViewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
            make.edges.mas_equalTo(UIEdgeInsetsZero);
        }];
        
        [UIView animateKeyframesWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
            [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.15 animations:^{
                toViewController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
            }];
            [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:[self transitionDuration:transitionContext] animations:^{
                toViewController.view.alpha = 1;
            }];
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    } else if (self.type == AnimationTypeDismiss) {
        //Add 'to' view to the hierarchy
        [containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
        [UIView animateKeyframesWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
            [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:[self transitionDuration:transitionContext] animations:^{
                fromViewController.view.alpha = 0;
            }];
            [UIView addKeyframeWithRelativeStartTime:0.15 relativeDuration:0.15 animations:^{
                fromViewController.view.transform = CGAffineTransformMakeScale(0.95, 0.95);
            }];
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    }
}

PS:从协议中两个方法可以看出,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从UIViewControllerContextTransitioning 协议的对象。通常情况下,当我们使用系统的类时,系统框架为我们提供的转场代理(Transitioning Delegates),为我们创建了转场上下文对象,并把它传递给动画控制器。

然后在oneVC中需要实现UINavigationControllerDelegate

    //**不要忘了设置代理**
    self.navigationController.delegate = self;

跳转还是正常的push

    [self.navigationController pushViewController:twoVC animated:YES];

然后在oneVC中实现以下代理方法:

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
   //这里需要判断下fromVC和toVC
    CustomAnimator *animationController = [[CustomAnimator alloc] init];
    switch (operation) {
        case UINavigationControllerOperationPush:
            animationController.type = AnimationTypePresent;
            return  animationController;
        case UINavigationControllerOperationPop:
            animationController.type = AnimationTypeDismiss;
            return animationController;
        default: return nil;
    }
}

有个问题需要注意下,修改了动画之后你需要指定下是哪个控制器需要这种弹出效果,在上面的代理方法里面注释处,如果不指定就默人修改了所有页面的push动画,这种自定义效果还是比较好的,但是实现起来也比前两种更为复杂,还是一句话,看需求来选择哪种方案实现。

另外:在twoVC中别忘了实现导航栏的隐藏:

- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    
    [self.navigationController setNavigationBarHidden:YES animated:animated];
}

再在threeVC中实现导航栏的显示:

- (void)viewWillAppear:(BOOL)animated {
    [self.navigationController setNavigationBarHidden:NO animated:animated];
}

好了,三种方案都已经介绍完了,希望可以对大家有所帮助。

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

推荐阅读更多精彩内容