【iOS 开发笔记】导航控制器的封装及转场详解

前言

导航控制器应该是iOS开发中用的最多的控制器之一了,怎样才能优雅的来管理导航呢?虽然这个问题的我还没有找到答案,不过想着封装一下总是要的,那么我们今天就一起来看下对导航控制器的封装。

第一部分:简单的样式封装

1 系统默认导航栏的样式和返回方式

系统默认导航栏大家应该都不陌生,左上角一个返回按钮,其文本默认为上一个ViewController的标题,如果上一个ViewController没有标题,则显示"返回"字样。
默认情况下,点击返回按钮或者右滑可以返回上一页面,这些不需要设置和操作,因此也没什么需要说明的。

2 自定义左上角的返回按钮

2.1 自定义按钮

大多数情况下,我们需要根据产品和UI的需求来自定义返回按钮,当然这对开发者来说不是什么难事,只需要创建一个UIBarButtonItem和一张图片,并添加相应的点击事件就好了。

/**设置默认导航返回按钮*/
-(void)default_setNavigationBarBackItem {
    UIButton *leftButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 25, 25)];
    [leftButton setImage:[UIImage imageNamed:@"back"] forState:UIControlStateNormal];
    [[leftButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        [self popViewControllerAnimated:true];
    }];
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:leftButton];
}

2.2 调整按钮位置

如果还想要调整按钮位置,设置frame显然是不行的,NavigationItem是一个比较特殊的view,无法通过frame来调整左右按钮的位置,但使用UIBarButtonSystemItemFixedSpace控件,我们就可以轻松实现调整按钮的位置。

/**设置默认导航返回按钮*/
-(void)default_setNavigationBarBackItem {
    UIButton *leftButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 25, 25)];
    [leftButton setImage:[UIImage imageNamed:@"back"] forState:UIControlStateNormal];
    [[leftButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        [self popViewControllerAnimated:true];
    }];
    UIBarButtonItem *leftButtonItem = [[UIBarButtonItem alloc] initWithCustomView:leftButton];
    //创建UIBarButtonSystemItemFixedSpace
    UIBarButtonItem * spaceItem = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    //将宽度设为负值
    spaceItem.width = -15;
    //将两个BarButtonItem都返回给NavigationItem
    self.navigationItem.leftBarButtonItems = @[spaceItem, leftButtonItem];
}

2.3 滑动返回手势

如果使用自定义按钮去替换系统默认的返回按钮,会出现滑动返回手势失效的情况,解决方法也很简单,只需要重新添加导航栏的interactivePopGestureRecognizerdelegate即可。

self.interactivePopGestureRecognizer.delegate = self;

3 设置系统默认标题字体和颜色

这里可以使用UIAppearance来设置统一样式。

//设置导航栏主题
- (void)setupNavigationBar
{
    UINavigationBar *appearance = [UINavigationBar appearance];
    //统一设置导航栏颜色,如果单个界面需要设置,可以在viewWillAppear里面设置,在viewWillDisappear设置回统一格式。
    [appearance setBarTintColor:[UIColor getColor:@"fb9c0a"]];

    //导航栏title格式
    NSMutableDictionary *textAttribute = [NSMutableDictionary dictionary];
    textAttribute[NSForegroundColorAttributeName] = [UIColor whiteColor];
    textAttribute[NSFontAttributeName] = [UIFont systemFontOfSize:15];
    [appearance setTitleTextAttributes:textAttribute];
}

只是一方面已经对导航栏进行了封装,另一方面项目中有可能某些模块的主题样式会发生变化,有时候我们会在viewWillAppear中设置新的样式,并在viewWillDisappear中还原样式,为了方便,我在自定义的继承自UINavigationController的子类中暴露出两个方法,来方便进行设置和还原。

/**还原默认设置*/
-(void)ten_resetNavigationBarSet {
    [self ten_setNavigationBarWithColor:UIColor.whiteColor andFont:[UIFont systemFontOfSize:18]];
    [self.navigationBar setBackgroundImage:[UIImage imageWithColor:UIColor.whiteColor size:CGSizeMake(kScreenWidth, kNavHeight)] forBarMetrics:UIBarMetricsDefault];
    // 去掉导航栏底部黑线,影响导航栏半透明
    [self.navigationBar setShadowImage:[UIImage new]];
}

/**设置导航栏的颜色和字体*/
-(void)ten_setNavigationBarWithColor:(UIColor *)color andFont:(UIFont *)font {
    [self.navigationBar setTitleTextAttributes:@{
        NSForegroundColorAttributeName:color,
        NSFontAttributeName:font,
    }];
}

4 第一部分小结

我们已经完成了一些简单常用属性的设置,对导航控制器完成了初步的封装,下面,将开始我们的重点,对导航控制器的动画转场进行封装。

第二部分:导航控制器的动画转场

在改变了导航栏样式后,我们有了一个看起来不错的导航栏,但是我们滑动时的切换依然是系统自带的动画效果,如果遇到前一个NavigationBar为透明或者前后颜色不一致的情况,这种渐变式的动画就不会太友好,尤其是当前后两个界面有一个页面的NavigationBar是隐藏的时候,效果更是惨不忍睹。
针对这种问题,我们模仿一些大厂的APP,如美团、淘宝等,使用一种整体返回的方案,将两个NavigationBar独立开来,可以相对完美的解决问题。
当然,通过自定义导航栏,不使用系统的导航栏也可以解决对应问题,具体实现也比较简单,我们这里就不再赘述,下面我们将使用第一种方案来进行实现。

1 基本思路及对Transition的解释

1.1 基本思路

在导航的交互中,push一般是通过点击事件触发的,而pop则可能通过点击事件或者右滑手势触发,因此我们要将实现动画的效果分为交互效果和无交互效果两种。
下面的文章中,我们将分别使用两种方式来实现交互效果和无交互效果的动画。

1.2 Transition

转场时发生了什么?


The Anatomy of Transition

转场过程中,作为容器的父 VC 维护着多个子 VC,但在视图结构上,只保留一个子 VC 的视图,所以转场的本质是下一场景(subVC)的视图替换当前场景(subVC)的视图以及相应的控制器(subVC)的替换,表现为当前视图消失和下一视图出现,基于此进行动画,动画的方式非常多,所以限制最终呈现的效果就只有你的想象力了。图中的 Parent VC 可替换为 UIViewController, UITabbarController 或 UINavigationController 中的任何一种。

iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活。转场协议由5种协议组成,在实际中只需要我们提供其中的两个或三个便能实现绝大部分的转场动画:

1.转场代理(Transition Delegate)
自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。有如下三种转场代理,对应上面三种类型的转场:

<UINavigationControllerDelegate> //UINavigationController 的 delegate 属性遵守该协议。
<UITabBarControllerDelegate> //UITabBarController 的 delegate 属性遵守该协议。
<UIViewControllerTransitioningDelegate> //UIViewController 的 transitioningDelegate 属性遵守该协议。

转场发生时,UIKit 将要求转场代理将提供转场动画的核心构件:动画控制器和交互控制器(可选的)由我们实现。

2.动画控制器(Animation Controller)
最重要的部分,负责添加视图以及执行动画;遵守协议;由我们实现。

3.交互控制器(Interaction Controller)
通过交互手段,通常是手势来驱动动画控制器实现的动画,使得用户能够控制整个过程;遵守UIViewControllerAnimatedTransitioning协议,系统已经打包好现成的类供我们使用。

4.转场环境(Transition Context)
提供转场中需要的数据;遵守UIViewControllerContextTransitioning协议,由 UIKit 在转场开始前生成并提供给我们提交的动画控制器和交互控制器使用。

5.转场协调器(Transition Coordinator):
可在转场动画发生的同时并行执行其他的动画,其作用与其说协调不如说辅助,主要在 Modal 转场和交互转场取消时使用,其他时候很少用到;遵守UIViewControllerTransitionCoordinator协议,由 UIKit 在转场时生成,UIViewController在 iOS 7 中新增了方法transitionCoordinator()返回一个遵守该协议的对象,且该方法只在该控制器处于转场过程中才返回一个此类对象,不参与转场时返回 nil。

总结下,5个协议只需要我们操心3个;实现一个最低限度可用的转场动画,我们只需要提供上面五个组件里的两个:转场代理和动画控制器即可,还有一个转场环境是必需的,不过这由系统提供;当进一步实现交互转场时,还需要我们提供交互控制器,也有现成的类供我们使用。

2 实现交互效果动画

这里的思路方案有两种。
1.通过每次Push前,对当前页面进行截图保存并保存到数组中,Pop时取数组最后一个元素显示,滑动结束后调用系统Pop方法并删除最后一张截图。
2.使用UIViewControllerInteractiveTransitioning协议来实现。

2.1 使用截图的方案实现

1.准备需要的数组的手势

/**遮罩默认初始透明度*/
static double ten_coverDefaultAlpha = 0.6;
/**当拖动的距离,占了屏幕的总宽高的3/4时, 就让imageview完全显示,遮盖完全消失*/
static double ten_targetTranslateScale = 0.75;

@interface TenNavigationController () <UINavigationControllerDelegate,UIGestureRecognizerDelegate>
//通用属性
/**是否允许手势*/
@property (nonatomic, assign, readonly) BOOL isAllowGesture;
//实现交互动画相关
/**屏幕截图展示*/
@property (nonatomic, strong) UIImageView *screenshotImageView;
/**过渡的封面*/
@property (nonatomic, strong) UIView *coverView;
/**屏幕截图的数据源*/
@property (nonatomic, strong) NSMutableArray *screenshotImageArray;

2.初始化方法

#pragma mark - 实现交互动画效果
/**初始化设置*/
-(void)animation_interactionSet {
    [self.view addGestureRecognizer:self.customPopGestureRecognizer];
}
#pragma mark - 懒加载
/**自定义手势*/
-(UIScreenEdgePanGestureRecognizer *)customPopGestureRecognizer {
    if (!_customPopGestureRecognizer) {
        _customPopGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(animation_panGestureRec:)];
        _customPopGestureRecognizer.delegate = self;
        _customPopGestureRecognizer.edges = UIRectEdgeLeft;
    }
    return _customPopGestureRecognizer;
}

/**截图显示的ImageView*/
-(UIImageView *)screenshotImageView {
    if (!_screenshotImageView) {
        _screenshotImageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight)];
    }
    return _screenshotImageView;
}

/**截图上面的黑色半透明遮罩*/
-(UIView *)coverView {
    if (!_coverView) {
        _coverView = [[UIView alloc]initWithFrame:self.screenshotImageView.frame];
        _coverView.backgroundColor = UIColor.blackColor;
    }
    return _coverView;
}

/**截图数据源*/
-(NSMutableArray *)screenshotImageArray {
    if (!_screenshotImageArray) {
        _screenshotImageArray = [NSMutableArray array];
    }
    return _screenshotImageArray;
}

/**是否允许手势*/
-(BOOL)isAllowGesture {
    return (self.viewControllers.count > 1);
}

3.实现手势的相应事件

/**自定义手势对应的方法*/
-(void)animation_panGestureRec:(UIPanGestureRecognizer *)panGestureRec {
    if (self.isAllowGesture) {
        //当前允许手势左滑
        switch (panGestureRec.state) {
            case UIGestureRecognizerStateBegan:
                //开始拖拽阶段
                [self animation_panDragBegin];
                break;
            case UIGestureRecognizerStateEnded:
                //结束拖拽阶段
                [self animation_panDragEnd];
                break;
            default:
                //正在拖拽阶段
                [self animation_panDragging:panGestureRec];
                break;
        }
    }
}

/**自定义手势开始拖拽,添加图片和遮罩*/
-(void)animation_panDragBegin {
    //1.每次开始pan手势时,都要添加截图imageView和遮罩cover到window中
    [self.view.window insertSubview:self.screenshotImageView atIndex:0];
    [self.view.window insertSubview:self.coverView aboveSubview:self.screenshotImageView];
    //2.让imageView显示截图数组中的最新的一张截图
    self.screenshotImageView.image = self.screenshotImageArray.lastObject;
}

/**自定义手势正在拖拽,进行位移和透明度变化*/
-(void)animation_panDragging:(UIPanGestureRecognizer *)pan {
    //得到手指拖动的位移
    CGFloat offset_x = [pan translationInView:self.view].x;
    //平移整个view
    if (offset_x > 0) {
        self.view.transform = CGAffineTransformMakeTranslation(offset_x, 0);
    }
    //计算目前手指拖动位移占屏幕的宽高比例,当这个比例达到0.75(3/4)时,让遮罩隐藏
    double currentTranslateScaleX = offset_x/self.view.frame.size.width;
    if (offset_x < kScreenWidth) {
        self.screenshotImageView.transform = CGAffineTransformMakeTranslation((offset_x-kScreenWidth)*0.6, 0);
    }
    //让遮罩透明度改变,直到减为0完全透明,默认遮罩初始透明度0.6,默认的比例-(当前平衡比例/目标平衡比例)*默认的比例
    double cover_alpha = ten_coverDefaultAlpha - (currentTranslateScaleX / ten_targetTranslateScale) * ten_coverDefaultAlpha;
    self.coverView.alpha = cover_alpha;
}

/**自定义手势结束拖动,将图片和遮罩移除*/
-(void)animation_panDragEnd {
    //取出平移的距离
    CGFloat translate_x = self.view.transform.tx;
    //取出宽度
    CGFloat width = self.view.frame.size.width;
    if (translate_x <= kScreenWidth/3) {
        //如果手指移动距离不到屏幕的一半,回弹
        [UIView animateWithDuration:0.3 animations:^{
            //让被右移的view回弹,只要清空transform即可
            self.view.transform = CGAffineTransformIdentity;
            //让imageView大小恢复默认
            self.screenshotImageView.transform = CGAffineTransformMakeTranslation(-kScreenWidth, 0);
            //遮罩恢复默认透明度
            self.coverView.alpha = ten_coverDefaultAlpha;
        } completion:^(BOOL finished) {
            //动画完成后,移除两个view,下次开始拖动时,再添加进来
            [self.screenshotImageView removeFromSuperview];
            [self.coverView removeFromSuperview];
        }];
    }else {
        //如果手指移动距离超过屏幕一半,往右边回弹
        [UIView animateWithDuration:0.3 animations:^{
            //让被右移的view完全挪到屏幕最右边,结束后,清空view的transform
            self.view.transform = CGAffineTransformMakeTranslation(width, 0);
            //让imageView位移还原
            self.screenshotImageView.transform = CGAffineTransformMakeTranslation(0, 0);
            //遮罩完全变为透明
            self.coverView.alpha = 0;
        } completion:^(BOOL finished) {
            //清空view的transform,否则下次drag会有问题
            self.view.transform = CGAffineTransformIdentity;
            //移除两个view
            [self.screenshotImageView removeFromSuperview];
            [self.coverView removeFromSuperview];
            //执行正常的pop操作,移除栈顶控制器,让真正的前一个控制器成为导航控制器的栈顶控制器
            [self popViewControllerAnimated:NO];
        }];
    }
}

4.实现截图保存功能

/**截图保存功能,在push前截图*/
-(void)animation_screenShoot {
    //将要被截图的vc,窗口的根控制器
    UIViewController *rootVc = self.view.window.rootViewController;
    //背景图片总的大小
    CGSize size = rootVc.view.frame.size;
    //开启上下文,截图出原图
    UIGraphicsBeginImageContextWithOptions(size, YES, 0.0);
    //要裁剪的矩形范围
    CGRect rect = CGRectMake(0, 0, kScreenWidth, kScreenHeight);
    //截图
    [rootVc.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
    //从上下文中取出UIImage
    UIImage *shootImage = UIGraphicsGetImageFromCurrentImageContext();
    //将截图添加到图片数组 - 分开实现,保存数组放在animation_screenShoot中
    if (shootImage) {
        [self.screenshotImageArray addObject:shootImage];
    }
    //结束上下文
    UIGraphicsEndImageContext();
}

5.重写Push和Pop方法
思路里已经说过,Push前保存截图,Pop后删除截图,所以下面对Push和Pop方法进行重载来实现我们的要求。

#pragma mark - 重写方法
/**重写Push方法*/
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (self.viewControllers.count >= 1) {
        //不是根控制器
        viewController.hidesBottomBarWhenPushed = YES;
        [self default_setNavigationBarBackItem];
        //截图
        [self animation_screenShoot];
    }
    if ([self.topViewController isEqual:viewController]) {
        //同一视图不push两遍
        return;
    }
    [super pushViewController:viewController animated:animated];
}

/**重写Pop方法*/
-(UIViewController *)popViewControllerAnimated:(BOOL)animated {
    //移除最后一张截图
    [self.screenshotImageArray removeLastObject];
    return [super popViewControllerAnimated:animated];
}

/**重写Pop方法*/
-(NSArray<__kindof UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated {
    for (NSInteger i = self.viewControllers.count-1; i>0; i--) {
        if (viewController == self.viewControllers[i]) {
            break;
        }
        //移除已经Pop的view的截图
        [self.screenshotImageArray removeLastObject];
    }
    return [super popToViewController:viewController animated:animated];
}

/**重写Pop方法*/
-(NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated {
    //回到了根视图,移除所有截图
    [self.screenshotImageArray removeAllObjects];
    return [super popToRootViewControllerAnimated:animated];
}

6.收尾
至此我们已经实现了交互式的切换动画,整体来讲是用一个全屏的手势来代替了系统原来的手势,我们可能会在一些页面禁用手势,可以在isAllowGesture里面的判断里增加相关判断来实现

/**判断是否关闭左滑返回功能*/
-(BOOL)private_checkIsAllowPopGesture {
    BOOL isAllow = YES;
    NSArray *notAllowArray = @[
        @"VC1",
        @"VC2"
    ];
    NSString *nowVcStr = NSStringFromClass([self.topViewController class]);
    for (NSString *className in notAllowArray) {
        if ([nowVcStr isEqualToString:className]) {
            isAllow = NO;
            break;
        }
    }
    return isAllow;
}

/**是否允许手势*/
-(BOOL)isAllowGesture {
    return (self.viewControllers.count > 1) && [self private_checkIsAllowPopGesture];
}

2.2 使用UIViewControllerInteractiveTransitioning协议

交互过程实际上是由转场环境对象UIViewControllerContextTransitioning来管理的,它提供了如下几个方法来控制转场的进度:

func updateInteractiveTransition(_ percentComplete: CGFloat)//更新转场进度,进度数值范围为0.0~1.0。
func cancelInteractiveTransition()//取消转场,转场动画从当前状态返回至转场发生前的状态。
func finishInteractiveTransition()//完成转场,转场动画从当前状态继续直至结束。

交互控制协议<UIViewControllerInteractiveTransitioning>只有一个必须实现的方法:

func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning)

在转场代理里提供了交互控制器后,转场开始时,该方法自动被 UIKit 调用对转场环境进行配置。
系统打包好的UIPercentDrivenInteractiveTransition中的控制转场进度的方法与转场环境对象提供的三个方法同名,实际上只是前者调用了后者的方法而已。系统以一种解耦的方式使得动画控制器,交互控制器,转场环境对象互相协作,我们只需要使用UIPercentDrivenInteractiveTransition的三个同名方法来控制进度就够了。
1.准备要用到的属性

/**是否开始转场*/
@property (nonatomic, assign) BOOL interactive;
/**转场控制器*/
@property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactionController;

2.初始化

#pragma mark - 实现交互动画效果
/**初始化设置*/
-(void)animation_interactionSet {
    [self.view addGestureRecognizer:self.customPopGestureRecognizer];
}
#pragma mark - 懒加载
/**自定义手势*/
-(UIScreenEdgePanGestureRecognizer *)customPopGestureRecognizer {
    if (!_customPopGestureRecognizer) {
        _customPopGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(animation_panGestureRec:)];
        _customPopGestureRecognizer.delegate = self;
        _customPopGestureRecognizer.edges = UIRectEdgeLeft;
    }
    return _customPopGestureRecognizer;
}

-(UIPercentDrivenInteractiveTransition *)interactionController {
    if (!_interactionController) {
        _interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
    }
    return _interactionController;
}

3.实现手势绑定的方法

/**自定义手势对应的方法*/
-(void)animation_panGestureRec:(UIPanGestureRecognizer *)panGestureRec {
    //根据移动距离计算交互过程进度
    CGFloat translationX = [panGestureRec translationInView:self.view].x;
    CGFloat translationBase = self.view.frame.size.width/3;
    CGFloat translationAbs = translationX > 0 ? translationX : -translationX;
    CGFloat percent = translationAbs > translationBase ? 1.0 : translationAbs/translationBase;
    switch (panGestureRec.state) {
        case UIGestureRecognizerStateBegan:
            //开始拖拽阶段
            //更新交互状态
            self.interactive = YES;
            [self popViewControllerAnimated:YES];
            break;
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
            //结束拖拽阶段和取消拖拽阶段
            if (percent > 0.5) {
                //完成转场
                [self.interactionController finishInteractiveTransition];
            }else {
                //取消转场
                [self.interactionController cancelInteractiveTransition];
            }
            //无论转场结果如何,回复为非交互装填
            self.interactive = NO;
            break;
        case UIGestureRecognizerStateChanged:
            //正在拖拽阶段
            //更新进度
            [self.interactionController updateInteractiveTransition:percent];
            break;
        default:
            break;
    }
}

4.提交交互控制器

//实现UINavigationControllerDelegate的代理方法
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
    return self.interactive ? self.interactionController : nil;
}

5.收尾
交互状态结束时并非转场过程的终点(此后动画控制器提供的转场动画根据交互结束时的状态继续或是返回到初始状态),而是由动画控制器来结束这一切,如果实现了animationEnded方法,将在转场动画结束后调用。

3 实现无交互效果动画

无交互效果的过场动画,顾名思义,这种过场动画是你无法控制的,回想一下,你在点了导航栏的返回或者在push到下一个页面时,你什么都不能做,只能等这个过场动画结束,然后才能继续操作下面的页面。

要实现无交互效果的过场动画,需要做3件事。
1.让控制器知道你要进行过场动画了,你需要设置代理来感知这件事。
2.当控制器代理知道过场动画将要开始时,会去寻找一个动画控制器,这个动画控制器是一个遵守了UIViewControllerAnimatedTransitioning协议的类,因此,你可以令任何类负担起这个职责,只要让其遵循这个协议即可,一旦代理找到了动画控制器,那么就会执行动画控制器中定义的过场动画,否则就会去执行系统默认的过场动画。
3.去动画控制器中实现具体的动画,由于遵循了UIViewControllerAnimatedTransitioning协议,所以需要实现两个required方法:
(1)transitionDuration:这个方法返回了自定义动画的执行时间.
(2)animateTransition:整个自定义动画的核心, 到底要执行什么样的动画均在该方法中定义.

实现原理:FromVC代表将要消失的视图控制器,ToVC表示将要展示的视图控制器。
我们要实现的效果:
Push的时候,FromVC往左移动,ToVC从屏幕右侧出现跟随FromVC左移直至FromVC消失,此时ToVC刚好完整显示在屏幕上。
Pop的时候,FromVC向右移动,ToVC从屏幕边缘出现跟随FromVC向右移动直至FromVC消失,此时ToVC刚好完整显示在屏幕上

3.1 使用截图的实现

实现的时候,我们依然需要将Push和Pop分开讨论,这里由于项目中没有使用这种方式,代码部分就直接贴参考代码了。

Pop
(1) 和交互式动画一样,每次Push时对屏幕截屏并保存,Pop的再次截屏但不保存
(2) 把Pop时截取的图片作为FromVC展示,把Push到这个界面时截取的图片作为ToVC展示
(3) 并对两张图片做位移动画,动画结束后移除两张图片

Push
(1) Push时先对当前屏幕截屏。
(2) 将截取的图片保存方便Pop回来时使用,并把这张图片作为这次Push的FromVC保存。
(3) 获取当前导航栏控制器对象,调整其Transform属性中的位移参数作为ToVC展示
(4) 对截图和导航栏做位移,动画结束后直接移除截屏图片

1.创建动画控制器

@interface TenNavigationAnimationController : NSObject<UIViewControllerAnimatedTransitioning>
/**动画的操作,push/pop*/
@property (nonatomic, assign) UINavigationControllerOperation navigationOperation;
@end

2.在代理中返回控制器

#pragma mark - UINavigationControllerDelegate代理方法
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
    TenNavigationAnimationController *animationController = [[TenNavigationAnimationController alloc] init];
    animationController.operation = operation;
    return animationController;
}

3.在动画控制器中实现转场动画

-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    UIImageView * screentImgView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight)];
    UIImage * screenImg = [self screenShot];
    screentImgView.image =screenImg;
    
    //取出fromViewController,fromView和toViewController,toView
    UIViewController * fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController * toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    
    CGRect fromViewEndFrame = [transitionContext finalFrameForViewController:fromViewController];
    fromViewEndFrame.origin.x = ScreenWidth;
    CGRect fromViewStartFrame = fromViewEndFrame;
    CGRect toViewEndFrame = [transitionContext finalFrameForViewController:toViewController];
    CGRect toViewStartFrame = toViewEndFrame;
    
    UIView * containerView = [transitionContext containerView];
    
    switch (self.navigationOperation) {
        case UINavigationControllerOperationPush:
        {
            [self.screenShotArray addObject:screenImg];
            //这句非常重要,没有这句,就无法正常Push和Pop出对应的界面
            [containerView addSubview:toView];
            toView.frame = toViewStartFrame;
            UIView * nextVC = [[UIView alloc]initWithFrame:CGRectMake(ScreenWidth, 0, ScreenWidth, ScreenHeight)];
            //将截图添加到导航栏的View所属的window上
            [self.navigationController.view.window insertSubview:screentImgView atIndex:0];
            nextVC.layer.shadowColor = [UIColor blackColor].CGColor;
            nextVC.layer.shadowOffset = CGSizeMake(-0.8, 0);
            nextVC.layer.shadowOpacity = 0.6;
            self.navigationController.view.transform = CGAffineTransformMakeTranslation(ScreenWidth, 0);
            
            [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
                self.navigationController.view.transform = CGAffineTransformMakeTranslation(0, 0);
                screentImgView.center = CGPointMake(-ScreenWidth/2, ScreenHeight / 2);
            } completion:^(BOOL finished) {
                [nextVC removeFromSuperview];
                [screentImgView removeFromSuperview];
                [transitionContext completeTransition:YES];
            }];
        }
            break;
        case UINavigationControllerOperationPop:
        {
            fromViewStartFrame.origin.x = 0;
            [containerView addSubview:toView];
            UIImageView * lastVcImgView = [[UIImageView alloc]initWithFrame:CGRectMake(-ScreenWidth, 0, ScreenWidth, ScreenHeight)];
            //若removeCount大于0  则说明Pop了不止一个控制器
            if (_removeCount > 0) {
                for (NSInteger i = 0; i < _removeCount; i ++) {
                    if (i == _removeCount - 1) {
                        //当删除到要跳转页面的截图时,不再删除,并将该截图作为ToVC的截图展示
                        lastVcImgView.image = [self.screenShotArray lastObject];
                        _removeCount = 0;
                        break;
                    } else {
                        [self.screenShotArray removeLastObject];
                    }
                }
            } else {
                lastVcImgView.image = [self.screenShotArray lastObject];
            }
            screentImgView.layer.shadowColor = [UIColor blackColor].CGColor;
            screentImgView.layer.shadowOffset = CGSizeMake(-0.8, 0);
            screentImgView.layer.shadowOpacity = 0.6;
            [self.navigationController.view.window addSubview:lastVcImgView];
            [self.navigationController.view addSubview:screentImgView];
            
            [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
                screentImgView.center = CGPointMake(ScreenWidth * 3 / 2 , ScreenHeight / 2);
                lastVcImgView.center = CGPointMake(ScreenWidth/2, ScreenHeight/2);
            } completion:^(BOOL finished) {
                [lastVcImgView removeFromSuperview];
                [screentImgView removeFromSuperview];
                [self.screenShotArray removeLastObject];
                [transitionContext completeTransition:YES];
            }];
        }
            break;
        default:
            break;
    }
}

/**返回动画时间*/
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.3;
}

3.2 不使用截图的实现

使用截图和不使用截图的实现原理是一样的,区别只是在于具体的转场上,一个使用了页面截图,另一个没有使用。
1.创建动画控制器

@interface TenNavigationAnimationController : NSObject<UIViewControllerAnimatedTransitioning>
/**动画的操作,push/pop*/
@property (nonatomic, assign) UINavigationControllerOperation operation;
@end

2.在代理中返回控制器

#pragma mark - UINavigationControllerDelegate代理方法
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
    TenNavigationAnimationController *animationController = [[TenNavigationAnimationController alloc] init];
    animationController.operation = operation;
    return animationController;
}

3.在动画控制器中实现转场动画

/**执行动画的地方,核心方法*/
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    //1.取值
    //获取参与转场的视图控制器和视图
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    
    //计算坐标
    CGRect fromViewEndFrame = [transitionContext initialFrameForViewController:fromViewController];
    CGRect fromViewStartFrame = fromViewEndFrame;
    CGRect toViewEndFrame = [transitionContext finalFrameForViewController:toViewController];
    CGRect toViewStartFrame = toViewEndFrame;
    
    //返回容器视图,转场动画发生的地方
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toView];
    
    //2.设置
    switch (self.operation) {
        case UINavigationControllerOperationPush:
            toViewStartFrame.origin.x += toViewEndFrame.size.width;
            break;
        case UINavigationControllerOperationPop:
            fromViewEndFrame.origin.x += fromViewStartFrame.size.width;
            [containerView sendSubviewToBack:toView];
            break;
        default:
            break;
    }
    fromView.frame = fromViewStartFrame;
    toView.frame = toViewStartFrame;
    
    //3.动画
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        fromView.frame = fromViewEndFrame;
        toView.frame = toViewEndFrame;
    } completion:^(BOOL finished) {
        //4.通知转场完成
        //考虑转场可能中途取消的情况,转场结束后恢复视图状态
        fromView.transform = CGAffineTransformIdentity;
        toView.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
//        [transitionContext completeTransition:YES];
    }];
}

/**返回动画时间*/
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.3;
}

参考链接:
https://github.com/seedante/iOS-Note/wiki/ViewController-Transition
https://www.jianshu.com/p/31f177158c9e

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