iOS-自定义转场动画

iOS中推出控制器的方式有两种:push和present,iOS的push动画基本上已经成为苹果的一个标志,最好不要自定义,不然和系统的动画不一样会显得不和谐。
关于present,更多的可参考:present和dismiss。

下面介绍如何自定义present方式的转场动画。

1. UIViewControllerTransitioningDelegate协议

想自定义转场动画的VC必须遵守UIViewControllerTransitioningDelegate协议,实现协议的如下方法:

- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [EOCPresentAnimator new];
}

- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed {
    return [EOCDismissAnimator new];
}

- (nullable id )interactionControllerForDismissal:(id )animator {
    return  interactiveTransition;
}

解释:

  1. 方法1是present的界面添加动画,返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议。
  2. 方法2是为dismiss的界面添加动画,返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议。
  3. 方法3是控制转场进度的类,返回的对象要遵守UIViewControllerInteractiveTransitioning协议。
    系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议,我们直接使用它的子类。

2. 自定义动画类

接下来我们自定义动画类,遵守UIViewControllerAnimatedTransitioning协议,实现协议的两个方法,如下:

PresentAnimator.m文件:

#import "EOCPresentAnimator.h"

@implementation EOCPresentAnimator

//时间
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext {
    return 2.f;
}

//动作 系统会自己调用
- (void)animateTransition:(id )transitionContext {
    
    //上下文对象包含了全部信息
    //获取容器View
    UIView *containerView = transitionContext.containerView;
    //获取到toView:也就是说从ViewCtrlA跳转到ViewCtrlB,toView是ViewCtrlB.view
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    [containerView addSubview:toView];
    
    //rect范围的偏移量,大于0在右半边,下边
    CGRect frame = CGRectOffset(toView.frame, 0.f, [UIScreen mainScreen].bounds.size.height);
    toView.frame = frame;
    
    [UIView animateWithDuration:2.f animations:^{
        toView.frame = CGRectOffset(toView.frame, 0.f, -[UIScreen mainScreen].bounds.size.height);
    } completion:^(BOOL finished) {
        //结束上下文
        [transitionContext completeTransition:YES];
    }];
}
@end

DismissAnimator.m文件:

#import "EOCDismissAnimator.h"

@implementation EOCDismissAnimator

- (NSTimeInterval)transitionDuration:(nullable id )transitionContext {
    return 2.f;
}

- (void)animateTransition:(id )transitionContext {
    
    UIView *containerView = transitionContext.containerView;
    
    //获取到toView:从ViewCtrlB dismiss 到ViewCtrlA   fromView是B, toView是A
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    
    CGRect finalFrame = CGRectOffset(fromView.frame, 0.f, [UIScreen mainScreen].bounds.size.height);
    
    //containerView里面有fromView了
    //把toView放到最下面
    [containerView insertSubview:toView atIndex:0];
    
    [UIView animateWithDuration:2.f animations:^{
        fromView.frame = finalFrame;
    } completion:^(BOOL finished) {
        //它肯定实现了移除fromView的操作  取消就不完成
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
        
        //自己移除fromView不行,如果不结束转场,transitionView还在
        //[fromView removeFromSuperview];
    }];
}
@end

解释:

  1. - (void)animateTransition:(id )transitionContext;方法会在从一个VC跳转到另一个VC的时候,系统自动调用。
  2. 上个方法的参数transitionContext(转场上下文)是转场的中间人,里面保存了fromVC、toVC、containerView以及completeTransition:方法等信息。

动画类创建完成之后,我们在animationControllerForPresentedController:方法和animationControllerForDismissedController:方法里面传入两个动画对象,如下:

#pragma mark - UIViewControllerTransitioningDelegate
//为present的界面添加动画, 返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议
- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [EOCPresentAnimator new];
}

//为dismiss的界面添加动画, 返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议
- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed {
    return [EOCDismissAnimator new];
}

调用:

EOCNextViewController *nextViewCtrl = [[EOCNextViewController alloc] init];
nextViewCtrl.transitioningDelegate = self;
[self presentViewController:nextViewCtrl animated:YES completion:nil];

效果图:
present和dismiss.gif

下面有个新需求,如何在灰色界面,通过下滑手势dismiss到上一个界面,这里我们就需要用到interactionControllerForDismissal:方法了。

#pragma mark - UIViewControllerTransitioningDelegate
//控制转场进度的类, 返回的对象要遵守UIViewControllerInteractiveTransitioning协议
//系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议, 我们直接使用它的子类
- (nullable id )interactionControllerForDismissal:(id )animator {
    return  interactiveTransition;
}

这个方法需要返回一个遵守UIViewControllerInteractiveTransitioning协议的对象,由于系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议,我们直接使用它的子类,代码如下:

EOCInteractiveTransition.h文件

//  控制转场进度的类

#import 

@interface EOCInteractiveTransition : UIPercentDrivenInteractiveTransition

- (void)transitionToViewController:(UIViewController *)toViewController;

@end

EOCInteractiveTransition.m文件

#import "EOCInteractiveTransition.h"

@interface EOCInteractiveTransition () {
    UIViewController *presentedViewController;
    BOOL shouldComplete; //是否拖拽了一半以上
}

@end

@implementation EOCInteractiveTransition

- (void)transitionToViewController:(UIViewController *)toViewController {
    
    presentedViewController = toViewController;
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
    [toViewController.view addGestureRecognizer:panGesture];
}

- (void)panAction:(UIPanGestureRecognizer *)gesture {
    
    switch (gesture.state) {
        case UIGestureRecognizerStateBegan:
            
            [presentedViewController dismissViewControllerAnimated:YES completion:nil];
            
            break;
        case UIGestureRecognizerStateChanged: {
            
            //监听当前滑动的距离
            CGPoint transitionPoint = [gesture translationInView:presentedViewController.view];
            NSLog(@"transitionPoint %@", NSStringFromCGPoint(transitionPoint));
            
            CGFloat ratio = transitionPoint.y/[UIScreen mainScreen].bounds.size.height;
            NSLog(@"ratio: %f", ratio);
            
            if (ratio >= 0.5) {
                shouldComplete = YES;
            } else {
                shouldComplete = NO;
            }
            [self updateInteractiveTransition:ratio];
        }
            break;
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            if (shouldComplete) {
                [self finishInteractiveTransition];
            } else {
                [self cancelInteractiveTransition];
            }
        }
            break;
        default:
            break;
    }
}
@end

调用:

EOCNextViewController *nextViewCtrl = [[EOCNextViewController alloc] init];
[interactiveTransition transitionToViewController:nextViewCtrl];
nextViewCtrl.transitioningDelegate = self;
[self presentViewController:nextViewCtrl animated:YES completion:nil];

效果图:
下滑dismiss.gif

注意点:

  1. 没present的时候,层级结构如下
最开始.png
  1. present之后,层级结构如下
present之后.png

可以发现:
① present之后多了一个UITransitionView,这个View是专门做转场的。
② present之后并没有把控制器或者view直接盖上去,而是先移除旧的再添加新的,这个和push不一样。

自定义转场动画以及自定义容器动画Demo:自定义转场动画

你可能感兴趣的:(iOS-自定义转场动画)