iOS 动画 自定义转场动画

4 阅读3分钟

自定义转场动画

两套体系

  1. Modal present / dismiss:协议 UIViewControllerTransitioningDelegate
  2. Navigation push / pop:协议 UINavigationControllerDelegate

核心动画协议:UIViewControllerAnimatedTransitioning(动画本体)

交互手势可选:UIViewControllerInteractiveTransitioning(侧滑交互式转场)


实现一个简单的Model present/dismiss动画

//A->B页面
// 设置代理为self.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    SecondViewController *s = [[SecondViewController alloc] init];
    s.modalPresentationStyle = UIModalPresentationFullScreen;
    s.transitioningDelegate = self;
    [self presentViewController:s animated:YES completion:nil];
}

UIViewControllerTransitioningDelegate这个协议,返回一个处理转场动画的对象。

@interface ViewController () <UIViewControllerTransitioningDelegate>
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
    MyAnimation *animation =[[MyAnimation alloc] init];
    animation.isPresent = true;
    return animation;
}

处理转场动画的这个对象需要遵守UIViewControllerAnimatedTransitioning。

@interface MyAnimation : NSObject <UIViewControllerAnimatedTransitioning>
// 代理对象无法区分是dismiss还是present。如果要用同一个对象。就需要添加一个字段进行区分。
@property (nonatomic) bool isPresent;
@end

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
    return 5;
}
// This method can only be a no-op if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    // 获取 fromVC 和 toVC
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
                
    // 获取容器视图
    UIView *containerView = [transitionContext containerView];
    
    // 这里的if-else的代码都一样,只是为了区分。顺便方便打印不同UIViewController.view的地址。
    // 实际开发中,可以根据需要确定是否用同一个动画。
    if (self.isPresent){
        NSLog(@"from %p", fromVC.view);
        NSLog(@"to %p", toVC.view);
        NSArray *subView =  containerView.subviews;
        for (int i=0;i< subView.count;i ++){
            NSLog(@"%p %d", subView[i], i);
        }
        
        // fromVC默认就添加到了containerView
        toVC.view.alpha = 0;
        // 将目标视图加入容器
        [containerView addSubview:toVC.view];
                
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toVC.view.alpha = 1;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    }else{
        // 退场动画
        NSLog(@"from %p", fromVC.view);
        NSLog(@"to %p", toVC.view);
        NSArray *subView =  containerView.subviews;
        for (int i=0;i< subView.count;i ++){
            NSLog(@"%p %d", subView[i], i);
        }
        // fromVC.view自动添加到了containerView .fromVC就是要退出的View
        toVC.view.alpha = 0;
        // 将目标视图加入容器
        [containerView addSubview:toVC.view];
                
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toVC.view.alpha = 1;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    }
}

同理,dismiss的转场动画也需要设置代理.

@interface SecondViewController () <UIViewControllerTransitioningDelegate>
@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor greenColor];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    self.transitioningDelegate = self;
    
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismisse  {
    MyAnimation *animation = [[MyAnimation alloc] init];
    animation.isPresent = false;
    return  animation;
}

实现一个简单的navigation的push和pop动画。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
    self.navigationController.delegate = self;   
}
@interface ViewController () <UINavigationControllerDelegate>

需要注意的是,如果是大小变化。

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC{
    MyAnimation *animation = [[MyAnimation alloc] init];
    if (operation == UINavigationControllerOperationPush){
        animation.isPush  = YES;
    }else{
        animation.isPush  = false;
    }
    return animation;
}

第二个页面pop的时候,会回调根视图控制器的代理方法。不需要再次delegate=self。

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    // 获取 fromVC 和 toVC
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
                
    // 获取容器视图
    UIView *containerView = [transitionContext containerView];
    
    if (self.isPush) {
        // fromVC.view会自动添加到当前视图
        [containerView addSubview:toVC.view];
        toVC.view.alpha = 0;
        [UIView animateWithDuration:5 animations:^{
            toVC.view.alpha = 1;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:finished];
        }];
            
    } else {
        // fromVC.view会自动添加到当前视图
        [containerView insertSubview:toVC.view aboveSubview:fromVC.view];
        toVC.view.alpha = 0;
        [UIView animateWithDuration:5 animations:^{
            toVC.view.alpha = 1;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:finished];
        }];
        
    }
}

hero动画。

掌握前面的知识点,就可以做hero动画。核心就是snapshotViewAfterScreenUpdates:NO。

将fromVC.view.hidden = yes。

主要,如果只是大小的变化,直接用snapshotViewAfterScreenUpdates就行。但是如果有颜色的变化。则需要在上面覆盖一个视图才能做动画。

结束后,记得恢复原来的状态。

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    // 获取 fromVC 和 toVC
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
                
    // 获取容器视图
    UIView *containerView = [transitionContext containerView];
    
    if (self.isPush) {
        ViewController *fromC = (ViewController *)fromVC;
        SecondViewController *toC = (SecondViewController *)toVC;
        
        
        UIView *fromAnimationView = [fromC animationView];
        UIView *toAnimationView = [toC animationView];
        
        
        // 这个UIView不能修改颜色了
        UIView *copyFromView = [fromAnimationView snapshotViewAfterScreenUpdates:NO];
        copyFromView.frame = CGRectMake(100, 200, 300, 300);
        
        
        [containerView addSubview:toC.view];
        [containerView addSubview:copyFromView];
        
        // 先隐藏之前的视图
        fromAnimationView.hidden = YES;
        toAnimationView.hidden = YES;
        
        
        
        toVC.view.alpha = 0;
        // copyFromView的无法做backgrounColor动画
//        copyFromView.backgroundColor = [UIColor greenColor];
        UIView *bgView = [[UIView alloc] initWithFrame:copyFromView.bounds];
        bgView.backgroundColor = [UIColor greenColor];
        [copyFromView addSubview:bgView];
        [UIView animateWithDuration:5 animations:^{
            toVC.view.alpha = 1;
            bgView.backgroundColor = [UIColor redColor];
            copyFromView.frame = CGRectMake(100, 200, 100, 100);
            bgView.frame = CGRectMake(0, 0, 100, 100);
            
            
        } completion:^(BOOL finished) {
            // 恢复原来的
            fromAnimationView.hidden = NO;
            toAnimationView.hidden = NO;
            [transitionContext completeTransition:finished];
        }];
            
    } else {
        
        // todo
        
    }
}