容器转场动画

  前言

  目前而言自定义继承与UIViewController的容器子类,并没有现成的可使用的API来进行动画(将一个视图控制器转场到另外一个)。本文中将介绍如何自定义视图控制器的容器类的转场动画(如从一个控制器视图推出另一个控制器视图)。

       视图控制器的推出目前看来有三种方式:

  1. UINavigationController的push和pop
  2. 模态推送的present和dismiss
  3. UITabbarViewController的界面跳转    

  在这里当然还是有方法可以进行容器视图的转场,transitionFromViewController:toViewController:duration:options:animations:completion:,这方法看似好用,但若是对每个容器转场动画实现相同的逻辑,那就得实现对应的若干个相同方法。这样便产生了严重的代码冗余。若我们通过指定一个转场的动画,让所有的转场都执行此动画就提高了其代码的复用性,也降低了其耦合性。因此在这里主要讲解如何来自定义转场动画。demo地址:转场动画-demo,各位可以通过demo与本文描述结合理解,这样更能加深印象!

  相关API

  在开始代码之前,先看看我们所需要用到的相关类别和接口

  1. 转场代理(Transitioning Delegate)根据不同的转场类型提供其所需要的动画控制类和交互控制类
  2. 动画控制类(Animation Controller)遵从UIViewControllerAnimatedTransitioning协议,并且负责执行实际的动画。
  3. 交互控制类(Interaction Controller)遵从UIViewControllerInteractiveTransition协议来控制可交互的转场动画
  4. 转场上下文(Transitioning Context)定义了转场时需要的元数据(比如转场所参与了的视图控制器和视图的属性),其遵从UIViewControllerContextTransitioning协议,并且这是由系统负责生成和提供
  5. 转场协调器(Transition Coordinators)可以在运行转场动画时,并行的运行其他动画。转场协调器遵从UIViewControllerTransitionCoordinator协议

  转场动画交互方式分两种,第一种是属于非交互式:必须要实现动画控制类(相当于我们平时的直接点击一个按钮然后present出另一个视图控制器),第二种交互式:必须要实现动画控制类和交互控制类(例如可以通过手势的滑动距离来控制转场动画的一个进度,一般应用中都可以通过手势的滑动来推出一个视图控制器)。当然不论哪种转场动画,都必须要设定一个转场代理。

  代码部分

      Present转场动画

      非交互式转场动画

      1.新建工程,给FirstVc视图控制器的视图配置背景色和一个点击按钮(按钮用于present下一个控制器),在推出第二控制器视图的时候要注意配置其转场动画代理

      2.创建一个视图控制器(SecondVc)用于被推出,给其配置另外一种背景色和一个返回按钮(用于dismiss)

      3.配置转场代理,以下为FirstVc的按钮点击事件中配置代码示例

    SecondViewController *secondVc = [[SecondViewController alloc] init];
    //UIModalPresentationFullScreen:由系统管理推出完成后的视图
    //UIModalPresentationCustom:自己管理推出完成后的视图
    secondVc.modalPresentationStyle = UIModalPresentationCustom;
    //配置转场代理
    secondVc.transitioningDelegate = self;
    [self presentViewController:secondVc animated:YES completion:nil];
    //此处需要注意不要将transitioningDelegate写为modalTransitionStyle。modalTransitionStyle是系统提供转场动画效果,效果较少。

  4.配置转场代理协议方法

  prsent推送时调用的方法,返回动画控制类

  1)animationControllerForPresentedController: presentingController: sourceController:dismiss推送时调用方法,返回动画控制类

    2)animationControllerForDismissedController:

    交互式present推送调用方法,返回交互控制类

      3)interactionControllerForPresentation:

      交互式dismiss推送调用方法,返回交互控制类

      4)interactionControllerForDismissal:

      iOS8后引入的转场动画控制器,可以在转场动画的同时利用此控制器配置其他动画,只有在配置presentationStyle为Custom类型才有效果

      5)presentationControllerForPresentedViewController: presentingViewController:sourceViewController:

      在非交互式的情况下3),4)方法默认返回nil对象,不进行交互。若此时强行返回一个交互控制类会导致视图不能正常推出,造成程序假死情况!

      5.新建一个类(BQTransitionAnimation)继承自NSObject对象,遵从UIViewControllerAnimatedTransitioning协议,协议方法有:

      配置转场动画时间

      1)transitionDuration:

      配置转场动画具体逻辑方法

      2)animateTransition:

      方法中自带参数transitionContext,即前文中所提到的转场上下文,我们可以通过获取上下文中的元数据。如下所示:

    UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

  从上下文中取出了fromVc和toVc,接着再给这些视图控制器的视图做相应的动画即是转场动画。其中需要注意的是fromVc和toVc的一个相对概念。fromVc代表的是从哪个视图控制器来,toVc代表的是要到哪个视图控制器去。如上述中prsent时fromVc即为ViewController,toVc为NextViewController。但若是dismiss时,fromVc就变为了NextViewController,toVc为ViewController。以下是一个完整的BQTransitionAnimation.m代码,其中成员变量_presenting 用于判断此时控制器是在进行present还是dismiss,其中的动画逻辑可以根据自己的实际需要来进行编写,笔者的动画逻辑可在demo中BQTransitionAnimation类中查看

    6.在Viewcontroller中配置转场代理协议方法,实现其中的prsent和dismiss方法,代码如下:

    - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [[BQTransitionAnimation alloc] initWithAnimationType:AnimationType_Alpha_Change];
}
    - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return [[BQTransitionAnimation alloc] initWithAnimationType:AnimationType_Rotate];
}

  根据以上代码思路,一个简单的自定义非交互式转场动画便完成了。

  交互式转场动画

      交互式转场动画需要在非交互式的基础上再增加一个交互控制类,并以手势的方式来进行交互。

    1.在FirstVc视图中添加一个边缘滑动手势(UIScreenEdgePanGestureRecognizer),并指定滑动方向,并添加手势响应方法,具体代码实例如下:

- (UIScreenEdgePanGestureRecognizer *)gesture {
    if (_gesture == nil) {
        _gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSliderEvent:)];
        _gesture.edges = UIRectEdgeRight;
    }
    return _gesture;
}
- (void)gestureSliderEvent:(UIScreenEdgePanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
        //模态推送按钮
        [self showNextVcBtnClickedEvent:nil];
    }
}
- (void)showNextVcBtnClickedEvent:(UIButton *)sender {
    BQPresentInteractionSecondVc *nextVc = [[BQPresentInteractionSecondVc alloc] init];
    nextVc.modalPresentationStyle = UIModalPresentationCustom;
    //将转场代理设置为单独的一个对象,这样方便控制其是否需要交互
    nextVc.transitioningDelegate = self.transitionDelegate;
    nextVc.preVc = self;
    //通过判断是否通过按钮点击推出
    if (sender != nil) {
        //按钮点击推出,则转场代理手势设置为nil
        self.transitionDelegate.gesture = nil;
    }else {
        //非按钮点击推出(手势交互),则将手势传入转场代理,转场代理根据此手势来进行交互比例判断
        self.transitionDelegate.gesture = self.gesture;
    }
    [self presentViewController:nextVc animated:YES completion:nil];
}

  2.接下来在FirstVc中配置转场代理,并新建一个转场代理(BQTransitioningDelegate)类,关键代码示例如下:

- (BQTransitioningDelegate *)transitionDelegate {
    if (_transitionDelegate == nil) {
        _transitionDelegate = [[BQTransitioningDelegate alloc] init];
    }
    return _transitionDelegate;
}

  BQTransitioningDelegate.m文件中关键代码

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator {
    if (self.gesture != nil) {
        return [[BQpercentDrivenInteractive alloc] initWithPanGesture:self.gesture];
    }
    return nil;
}
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{
    if (self.gesture != nil) {
        return [[BQpercentDrivenInteractive alloc] initWithPanGesture:self.gesture];
    }
    return nil;
}

  3.创建一个类(BQpercentDrivenInteractive)继承自UIPercentDrivenInteractiveTransition。其内部的主要方法是

    1)startInteractiveTransition:(开启交互)

    2)updateInteractiveTransition:(更新交互动画)

    3)finishInteractiveTransition(完成交互动画)

    4)cancelInteractiveTransition(取消交互动画)

    在类中根据传入的手势添加一个手势响应方法,通过其响应可以计算出手势完成比例,再根据此比例数值更新其交互动画,具体事例代码如下:

- (instancetype)initWithPanGesture:(UIScreenEdgePanGestureRecognizer *)gesture {
    self = [super init];
    if (self) {
        _gesture = gesture;
        //添加手势触发事件
        [_gesture addTarget:self action:@selector(updateViewContorllerTransition:)];
    }
    return self;
}
- (void)dealloc {
    [_gesture removeTarget:self action:@selector(updateViewContorllerTransition:)];
}
//根据手势状态来更新交互动画操作
- (void)updateViewContorllerTransition:(UIScreenEdgePanGestureRecognizer *)sender {
    switch (sender.state) {
        case UIGestureRecognizerStateBegan:
            
            break;
        case UIGestureRecognizerStateChanged:
            [self updateInteractiveTransition:[self completPercentFromGesture]];
            break;
        case UIGestureRecognizerStateEnded:
        {
            if ([self completPercentFromGesture] >= 0.5) {
                [self finishInteractiveTransition];
            }else {
                [self cancelInteractiveTransition];
            }
        }
            break;
        default:
            [self cancelInteractiveTransition];
            break;
    }
}
//开始交互动画时调用
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    _transitionContext = transitionContext;
    [super startInteractiveTransition:transitionContext];
}
//根据手势计算当前手势的完成度
- (CGFloat)completPercentFromGesture {
    UIView *sourceView = _transitionContext.containerView;
    CGPoint point = [_gesture locationInView:sourceView];
    CGFloat percent = 0;
    if (_gesture.edges == UIRectEdgeRight) {
        percent = (Screen_Width - point.x) / Screen_Width;
    }else {
        percent = point.x / Screen_Width;
    }
    NSLog(@"%lf",percent);
    return percent;
}

  4.在SecondVc视图控制器中同样添加一个滑动手势,并同First中做出同样配置。当由手势交互引发视图控制器dismiss时,先将转场代理的手势配置为SecondVc中的滑动手势。这样就可以进行反向的交互转场动画了,关键示例代码如下:

- (void)backBtnClickedEvent:(UIButton *)sender {
    if (sender != nil) {
        ((BQTransitioningDelegate *)self.preVc.transitionDelegate).gesture = nil;
    }else{
        __weak typeof(self) weakSelf = self;
        ((BQTransitioningDelegate *)self.preVc.transitionDelegate).gesture = weakSelf.gesture;
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (void)gestureSliderChange:(UIScreenEdgePanGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateBegan) {
        [self backBtnClickedEvent:nil];
    }
}

  至此Present交互式的转场动画就告一段落。

  Navagation的转场动画

    Navagation配置同Present方法基本相同。首先只需要配置Navigation代理,接下来实现其代理方法即可。其中思路Present相同,代码示例如下:

self.navigationController.delegate = self;
#pragma mark - UINavigationControllerDelegate Method
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
    return [[BQTransitionAnimation alloc] initWithAnimationType:AnimationType_Alpha_Change];
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
    if (_isInteraction == YES) {
        return [[BQpercentDrivenInteractive alloc] initWithPanGesture:self.panGesture];
    }
    return nil;
}

  TabbarController转场动画

    关于tabbarController的转场动画配置同上面两种一样,此处并没有实现交互式转场动画。因为tabbarController在项目中一般是作为容器使用,其内部包含的视图一般包含有其他手势,所以对tabbarController做交互动画的极容易产生手势冲突。所以只需要配置其非交互是转场动画设置即可。自定义的转场动画就讲到这里,关于更多的细节部分(包括转场动画控制器、协调器的基本使用),请参照demo学习研究。如果上述有任何错误欢迎指正!

posted @ 2016-05-11 15:36  紫色大番薯  阅读(575)  评论(0编辑  收藏  举报