自定义UINavigationController push和pop动画无标题文章

前言

首先, push/pop 动效在iOS7.0以后系统就已经提供了相关的代理方法, 在代理方法中我们可以自定义切换动画.
自定义push,pop动画是由UINavigationController的代理方法中提供的.
用于实现自定义动画的代理(UINavigationControllerDelegate)方法如下:

- (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. 实现自定义push,pop动画

一般的自定义动画通过navigationController:animationControllerForOperation:fromViewController:toViewController:这个代理方法实现, 这个代理方法返回id<UIViewControllerAnimatedTransitioning>类型的对象, 也就是说, 只需要定义好实现UIViewControllerAnimatedTransitioning协议的一个类即可.

1. UIViewControllerAnimatedTransitioning协议

UIViewControllerAnimatedTransitioning协议定义如下所示, 不解释:

@protocol UIViewControllerAnimatedTransitioning <NSObject>

// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to
// synchronize with the main animation. 
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@optional

// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;

@end

代码中实现这个协议时候思路是这样的, 整体上是继承的结构, 因为动画具体的动效效果可能有n种, 但是基本方法和判断都是相同的. 父类遵循协议并实现如上的几个代理, 父类实现几个类方法来快速创建对象, 并提供其它初始化方法; 父类完成基本的逻辑判断和方法调用, 子类继承父类, 具体实现动效效果. 由于Objective-C中没有抽象方法, 例子在父类中声明方法, 空实现, 子类重写父类相关的方法. 父类如下所示:

WHBaseAnimationTransitioning.h

#import <UIKit/UIKit.h>
#import "UIViewController+WHAnimationTransitioningSnapshot.h"

@interface WHBaseAnimationTransitioning : NSObject <UIViewControllerAnimatedTransitioning>

@property (nonatomic, strong, readwrite) UIPercentDrivenInteractiveTransition *interactivePopTransition;

/// 创建动画效果的实例对象并设置动画类型, push or pop
+ (instancetype)transitionWithType:(UINavigationControllerOperation)transitionType;

/// 创建动画效果的实例对象并设置动画类型和间隔时间
+ (instancetype)transitionWithType:(UINavigationControllerOperation)transitionType duration:(NSTimeInterval)duration;

/// 创建动画效果实例对象并设置动画类型/间隔时间/可交互属性
+ (instancetype)transitionWithType:(UINavigationControllerOperation)transitionType duration:(NSTimeInterval)duration interactivePopTransition:(UIPercentDrivenInteractiveTransition *)interactivePopTransition;

- (instancetype)initWithType:(UINavigationControllerOperation)transitionType duration:(NSTimeInterval)duration;

#pragma mark - 真正实现 push, pop 动画的方法, 具体实现交给子类
- (void)push:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)pop:(id<UIViewControllerContextTransitioning>)transitionContext;

- (void)pushEnded;
- (void)popEnded;

@end

WHBaseAnimationTransitioning.m

#import "WHBaseAnimationTransitioning.h"

/// 默认动画执行时间间隔
const static NSTimeInterval WHAnimationTransitioningDuration = 0.6;

@interface WHBaseAnimationTransitioning ()

@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, assign) UINavigationControllerOperation transitionType;

@end

@implementation WHBaseAnimationTransitioning

- (instancetype)init {

    if (self = [self initWithType:UINavigationControllerOperationPush duration:WHAnimationTransitioningDuration]) {
    }
    return self;
}

// 主要的构造方法
- (instancetype)initWithType:(UINavigationControllerOperation)transitionType duration:(NSTimeInterval)duration {

    if (self = [super init]) {
        self.duration = duration;
        self.transitionType = transitionType;
    }
    return self;
}

+ (instancetype)transitionWithType:(UINavigationControllerOperation)transitionType {

    return [self transitionWithType:transitionType duration:WHAnimationTransitioningDuration];
}

+ (instancetype)transitionWithType:(UINavigationControllerOperation)transitionType
                          duration:(NSTimeInterval)duration {

    return [self transitionWithType:transitionType duration:duration interactivePopTransition:nil];
}

+ (instancetype)transitionWithType:(UINavigationControllerOperation)transitionType duration:(NSTimeInterval)duration interactivePopTransition:(UIPercentDrivenInteractiveTransition *)interactivePopTransition {

    WHBaseAnimationTransitioning *animationTransitioning = [[self alloc] initWithType:transitionType duration:duration];
    animationTransitioning.interactivePopTransition = interactivePopTransition;
    return animationTransitioning;
}

- (void)push:(id<UIViewControllerContextTransitioning>)transitionContext {}
- (void)pop:(id<UIViewControllerContextTransitioning>)transitionContext {}
- (void)pushEnded {}
- (void)popEnded {}

#pragma mark - UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {

    return self.duration;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {

    if (self.transitionType == UINavigationControllerOperationPush) {
        [self push:transitionContext];
    }
    else if (self.transitionType == UINavigationControllerOperationPop) {
        [self pop:transitionContext];
    }
}

- (void)animationEnded:(BOOL) transitionCompleted {

    if (!transitionCompleted) return;

    if (self.transitionType == UINavigationControllerOperationPush) {
        [self pushEnded];
    }
    else if (self.transitionType == UINavigationControllerOperationPop) {
        [self popEnded];
    }
}

@end

2. 具体实现动画

动画效果如下:

代码如下:

#import "WHBaseAnimationTransitioning.h"

@interface WHBackPriorViewAnimation : WHBaseAnimationTransitioning

@end

@implementation WHBackPriorViewAnimation

- (void)push:(id<UIViewControllerContextTransitioning>)transitionContext {

    UIViewController * fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController * toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    NSTimeInterval duration = [self transitionDuration:transitionContext];

    CGRect bounds = [[UIScreen mainScreen] bounds];
    fromVc.view.hidden = YES;
    [[transitionContext containerView] addSubview:fromVc.snapshot];
    [[transitionContext containerView] addSubview:toVc.view];
    [[toVc.navigationController.view superview] insertSubview:fromVc.snapshot belowSubview:toVc.navigationController.view];
    toVc.navigationController.view.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(bounds), 0.0);

    [UIView animateWithDuration:duration
                          delay:0
         usingSpringWithDamping:1.0
          initialSpringVelocity:0.0f
                        options:UIViewAnimationOptionCurveLinear
                     animations:^{
                         fromVc.snapshot.alpha = 0.3;
                         fromVc.snapshot.transform = CGAffineTransformMakeScale(0.965, 0.965);
                         toVc.navigationController.view.transform = CGAffineTransformMakeTranslation(0.0, 0.0);
                     }
                     completion:^(BOOL finished) {
                         fromVc.view.hidden = NO;
                         [fromVc.snapshot removeFromSuperview];
                         [toVc.snapshot removeFromSuperview];
                         [transitionContext completeTransition:YES];
                     }];
}

- (void)pop:(id<UIViewControllerContextTransitioning>)transitionContext {

    UIViewController * fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController * toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    CGRect bounds = [[UIScreen mainScreen] bounds];

    [fromVc.view addSubview:fromVc.snapshot];
    fromVc.navigationController.navigationBar.hidden = YES;
    fromVc.view.transform = CGAffineTransformMakeTranslation(0.0, 0.0);

    toVc.view.hidden = YES;
    toVc.snapshot.alpha = 0.3;
    toVc.snapshot.transform = CGAffineTransformMakeScale(0.965, 0.965);

    [[transitionContext containerView] addSubview:toVc.view];
    [[transitionContext containerView] addSubview:toVc.snapshot];
    [[transitionContext containerView] sendSubviewToBack:toVc.snapshot];

    [UIView animateWithDuration:duration
                          delay:0
         usingSpringWithDamping:1.0
          initialSpringVelocity:0.1f
                        options:UIViewAnimationOptionCurveLinear
                     animations:^{
                         fromVc.view.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(bounds), 0.0);
                         toVc.snapshot.alpha = 1.0;
                         toVc.snapshot.transform = CGAffineTransformIdentity;
                     }
                     completion:^(BOOL finished) {

                         toVc.navigationController.navigationBar.hidden = NO;
                         toVc.view.hidden = NO;

                         [fromVc.snapshot removeFromSuperview];
                         [toVc.snapshot removeFromSuperview];
                         fromVc.snapshot = nil;

                         // Reset toViewController's `snapshot` to nil
                         if (![transitionContext transitionWasCancelled]) {
                             toVc.snapshot = nil;
                         }

                         [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
                     }];
}

@end

到此为止, 已经成功实现自定义push, pop动画.

2. 实现可交互的pop动画

交互式的动画, 这个概念太大, 这里介绍的是使用手势pop动画的实现过程.

1. 动画效果

动画效果如下所示:

2. 实现过程

手势pop动画, 主要是要监听手势变化, 然后根据手势变化更新动画. 我的实现思路是, 在BaseViewController中给控制器添加手势并监听, 如果是右滑则开始执行pop动画, 具体代码如下所示:

// 判断是否是根控制器 并添加手势
if (self.navigationController != nil && self != self.navigationController.viewControllers.firstObject) {
        UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopRecognizer:)];
        [self.view addGestureRecognizer:popRecognizer];
        popRecognizer.delegate = self;
    }

#pragma mark - UIPanGestureRecognizer handlers
// 实现监听手势变化的代理方法
- (void)handlePopRecognizer:(UIPanGestureRecognizer *)recognizer {

    CGFloat progress = [recognizer translationInView:self.view].x / CGRectGetWidth(self.view.frame);
    progress = MIN(1.0, MAX(0.0, progress));

    if (recognizer.state == UIGestureRecognizerStateBegan) {

        // Create a interactive transition and pop the view controller
        self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];

        [self.navigationController popViewControllerAnimated:YES];
    } else if (recognizer.state == UIGestureRecognizerStateChanged) {

        // Update the interactive transition's progress
        [self.interactivePopTransition updateInteractiveTransition:progress];
    } else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {

        // Finish or cancel the interactive transition
        if (progress > 0.25) {
            [self.interactivePopTransition finishInteractiveTransition];
        } else {
            [self.interactivePopTransition cancelInteractiveTransition];
        }

        self.interactivePopTransition = nil;
    }
}

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)recognizer {
    return [recognizer velocityInView:self.view].x > 0;
}

BaseViewController中实现以上方法即可, 要想实现侧滑功能, 还需要实现NAVi的一个代理方法:navigationController:interactionControllerForAnimationController:, 在这个代理方法中返回遵循UIViewControllerInteractiveTransitioning协议的对象即可.

- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                   interactionControllerForAnimationController:(WHBaseAnimationTransitioning *) animationController  {

    return animationController.interactivePopTransition;
}

至此, 手势实现pop效果已经实现完毕, 如有任何疑问, 欢迎交流.

另有其它几种动效效果, 详见github源码.

如果您觉得本文有用, 欢迎star支持一下 😃 .

源码地址: https://github.com/hell03W/SwitchControllerAnimation

后记

博文作者:hell03W

博文出处: http://my.oschina.net/whforever

github: https://github.com/hell03W

oschina: http://my.oschina.net/whforever

jianshu: //www.greatytc.com/users/ea059360a6f6

本文版权归作者,欢迎转载,但须保留此段声明,并给出原文链接,谢谢合作!

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

推荐阅读更多精彩内容