TransitionAnimation自定義轉場動畫

Misaki發表於2018-12-04

    在iOS 7之後,蘋果就開放了自定義轉場的相關api,現在都快iOS 12了,一直都沒有好好研究轉場動畫,一個是之前沒有重視,覺得花裡胡哨的,另外一個是所做的專案中沒有這樣的轉場動畫需求。這裡說的轉場動畫和上一篇CAAnimation 系統動畫CATransition動畫不是一個概念,上一篇指的是單個View的轉場特效,這裡指的是整個控制器的轉場特效。其實寫上篇文章的目前也是為今天打下鋪墊,複雜的轉場效果也是由單個動畫來組成的。

自定義轉場動畫類圖

    由圖中可以看出要完成自定義轉場動畫,必須遵從UIViewControllerAnimatedTransitioning協議,協議中有兩個必須實現的方法一個是返回轉場時間,一個是具體轉場的實現。文章會結合5個最常用的動畫場景來說明轉場動畫。

    先來看看網易嚴選App的轉場效果,可以看出當前頁面想要Push其他的頁面的時候,當前頁面會下沉同時其他頁面從右邊平移至左邊。Present頁面的時候,當前頁面也會下沉,目標檢視從底部彈出。

網易嚴選Push和Pop動畫.gif

網易嚴選Present和Dismiss動畫.gif

    來看程式碼,在ViewController裡面有兩個按鈕,分別是PushSecondVCPresentThirdVC

- (IBAction)pushBtnClick:(id)sender
{
    SecondViewController * vc = [[SecondViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}


- (IBAction)presentBtnClick:(id)sender
{
    ThirdViewController * vc = [[ThirdViewController alloc] init];
    [self presentViewController:vc animated:YES completion:nil];
}

複製程式碼

Push和Pop動畫

UIViewControllerAnimatedTransitioning協議

    這裡新建一個AnimatedTransitioningObject類,然後要遵循UIViewControllerAnimatedTransitioning協議。這個為了方便,把Push、Pop、Present、Dismiss這四個效果寫在一起,用列舉來區分,當然也可以把每種動畫效果單獨用一個AnimatedTransitioningObject類來實現。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger,TransitionAnimationObjectType) {
    TransitionAnimationObjectType_Push,
    TransitionAnimationObjectType_Pop,
    TransitionAnimationObjectType_present,
    TransitionAnimationObjectType_Dismiss
};

@interface TransitionAnimationObject : NSObject <UIViewControllerAnimatedTransitioning>

@property (nonatomic,assign) TransitionAnimationObjectType type;

- (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;

+ (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;

@end
複製程式碼

    來看看兩個必須實現的方法,在返回轉場時間裡也可以根據type來返回不同的動畫時間,這裡統一返回0.5秒。pushAnimateTransition裡面實現Push效果轉場。

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    switch (_type) {
        case TransitionAnimationObjectType_Push:
            [self pushAnimateTransition:transitionContext];
            break;

        case TransitionAnimationObjectType_Pop:
            [self popAnimateTransition:transitionContext];
            break;

        case TransitionAnimationObjectType_present:
            [self presentAnimateTransition:transitionContext];
            break;

        case TransitionAnimationObjectType_Dismiss:
            [self dismissAnimateTransition:transitionContext];
            break;

        default:
            break;
    }
}
複製程式碼

Push實現

- (void)pushAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //獲取目標View(secondVC.view) 和 來源View(ViewController.view)
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //這裡截圖做動畫 隱藏來源View
    UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
    fromView.hidden = YES;

    //將需要做轉場的View按照順序新增到轉場容器中
    UIView * containerView = [transitionContext containerView];
    [containerView addSubview:tempView];
    [containerView addSubview:toView];

    CGFloat width = containerView.frame.size.width;
    CGFloat height = containerView.frame.size.height;

    //設定目標View的初始位置
    toView.frame = CGRectMake(width, 0, width, height);

    //開始做動畫
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration animations:^{
        tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
        toView.transform = CGAffineTransformMakeTranslation(-width, 0);
    } completion:^(BOOL finished) {
        //這裡要標記轉場成功 假如不標記 系統會認為還在轉場中 無法互動
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];

        //轉場失敗 也要做相應的處理
        if ([transitionContext transitionWasCancelled])
        {
            fromView.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];

}
複製程式碼

Pop實現

     PushPop是相對的關係,所以在Pop動畫中,目標檢視和來源檢視互換身份,實現也是用CGAffineTransformIdentity來還原Push動畫即可。

- (void)popAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //注意這裡是還原 所以toView和fromView 身份互換了 toView是ViewController.view
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //獲取相應的檢視
    UIView * containerView = [transitionContext containerView];
    UIView * tempView = [[containerView subviews] firstObject];

    //在fromView 下面插入toView 不然回來的時候回黑屏
    [containerView insertSubview:toView belowSubview:fromView];

    //將動畫直接還原即可
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration animations:^{
        tempView.transform = CGAffineTransformIdentity;
        fromView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        //標記轉場
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];

        //轉場成功的處理
        if (![transitionContext transitionWasCancelled])
        {
            [tempView removeFromSuperview];
            toView.hidden = NO;
        }
    }];
}

複製程式碼

UINavigationControllerDelegate代理方法

    完成AnimatedTransitioningObject類後,再返回ViewController中,ViewController要遵循UINavigationBarDelegateUIViewControllerTransitioningDelegate,把SecondVCtransitioningDelegate設定為自己。然後根據不同的operation,來返回不同的動畫實現。

@interface ViewController () <UINavigationControllerDelegate,UIViewControllerTransitioningDelegate>

- (IBAction)pushBtnClick:(id)sender
{
    SecondViewController * vc = [[SecondViewController alloc] init];
    vc.transitioningDelegate = self;
    [self.navigationController pushViewController:vc animated:YES];
}

#pragma mark - Push && Pop
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
    if (operation == UINavigationControllerOperationPush)
    {
        return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Push];
    }
    else if (operation == UINavigationControllerOperationPop)
    {
        return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Pop];
    }
    return nil;
}
複製程式碼

    看看實現效果

Push和Pop效果.gif

Present動畫和Dismiss動畫

Present實現

- (void)presentAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //獲取目標View(ThirdVC.view) 和 來源View(ViewController.view)
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //截圖做動畫
    UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
    tempView.frame = fromView.frame;
    fromView.hidden = YES;

    //按照順序假如轉場動畫容器中
    UIView * containerView = [transitionContext containerView];
    [containerView addSubview:tempView];
    [containerView addSubview:toView];

    CGFloat width = containerView.frame.size.width;
    CGFloat height = containerView.frame.size.height;

    //設定toView的初始化位置 在螢幕底部
    toView.frame = CGRectMake(0, height, width, 400);

    //做轉場動畫
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
        tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
        toView.transform = CGAffineTransformMakeTranslation(0, -400);
    } completion:^(BOOL finished) {
        //轉場結束後一定要標記 否則會認為還在轉場 無法互動
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if ([transitionContext transitionWasCancelled])
        {
            //轉場失敗
            fromView.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];
}
複製程式碼

Dismiss實現

- (void)dismissAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    //dismiss的時候 fromVC和toVC身份倒過來了
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    //containerView裡面的順序也倒過來了 截圖在最上面
    UIView * containerView = [transitionContext containerView];
    UIView * tempView = [[containerView subviews] firstObject];

    //做還原動畫就可以了
    NSTimeInterval duration = [self transitionDuration:transitionContext];

    [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
        tempView.transform = CGAffineTransformIdentity;
        fromView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        //轉場結束後一定要標記 否則會認為還在轉場 無法互動
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if (![transitionContext transitionWasCancelled])
        {
            //轉場成功
            toView.hidden = NO;
            [tempView removeFromSuperview];
        }
    }];

}
複製程式碼

UIViewControllerTransitioningDelegate代理方法

    回到ViewController,把ThirdVCtransitioningDelegate設定為自己,然後在代理方法中自定型別。

- (IBAction)presentBtnClick:(id)sender
{
    ThirdViewController * vc = [[ThirdViewController alloc] init];
    vc.transitioningDelegate = self;
    [self presentViewController:vc animated:YES completion:nil];
}

#pragma mark - Present && Dismiss
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_present];
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Dismiss];
}
複製程式碼

手勢動畫

UIPercentDrivenInteractiveTransition建立手勢類

    新建一個手勢類GestureObject繼承自UIPercentDrivenInteractiveTransitionaddGestureToViewController是給目標控制器新增手勢。

#import <UIKit/UIKit.h>

@interface GestureObject : UIPercentDrivenInteractiveTransition

//判斷是互動的手勢
@property (nonatomic,assign) BOOL interacting;

- (void)addGestureToViewController:(UIViewController *)viewController;

@end
複製程式碼

    然後再手勢的狀態之間來判斷是否執行動畫,這裡是判斷手勢偏移量超過螢幕一半的高度就生效,執行相關動畫,否則還原動畫。

- (void)handleGesture:(UIPanGestureRecognizer *)ges
{
    CGPoint point = [ges translationInView:ges.view];

    switch (ges.state) {
        case UIGestureRecognizerStateBegan:
        {
            self.interacting = YES;
            [self.targetVC dismissViewControllerAnimated:YES completion:nil];
            break;
        }

        case UIGestureRecognizerStateChanged:
        {
            CGFloat fraction = point.y / ges.view.frame.size.height;
            //限制在0和1之間
            fraction = MAX(0.0, MIN(fraction, 1.0));
            self.shouldComplete = fraction > 0.5;
            [self updateInteractiveTransition:fraction];
            break;
        }
        
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
        {
            self.interacting = NO;
            if (!self.shouldComplete || ges.state == UIGestureRecognizerStateCancelled)
            {
                //還原動畫
                [self cancelInteractiveTransition];
            }
            else
            {
                //完成動畫
                [self finishInteractiveTransition];
            }
            break;
        }

        default:
            break;
    }
}
複製程式碼

UIViewControllerTransitioningDelegate代理方法

    回到ViewController中,在PresentThirdVC的時候新增手勢,在代理方法interactionControllerForDismissal中指定手勢。

- (IBAction)presentBtnClick:(id)sender
{
    ThirdViewController * vc = [[ThirdViewController alloc] init];
    vc.transitioningDelegate = self;
    [self.gestureObject addGestureToViewController:vc];
    [self presentViewController:vc animated:YES completion:nil];
}

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
    return self.gestureObject.interacting ? self.gestureObject : nil;
}
複製程式碼

看看效果

Present和Dismiss效果.gif

小結

     PushPopPresentDismiss、手勢動畫都講解完了,可以看出,自定義轉場大致的步驟是

  • 根據viewForKey來獲取轉場上下文
  • 將要轉場的檢視加入轉場容器中
  • 做出轉場動畫
  • 標記轉場成功的狀態,根據狀態做相應的處理

    理解了這些,再複雜的轉場動畫都能一步步分解出來,下面是格瓦拉App的轉場效果,第一次看的時候,覺得很酷炫,現在瞭解了轉場的核心後,覺得不那麼難了,有時間再把它的效果寫出來吧。

格瓦拉轉場動畫.gif

原始碼:TransitionAnimation

相關文章