TransitionAnimation自定義轉場動畫

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

自定義轉場動畫類圖

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

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

網易嚴選Push和Pop動畫.gif

網易嚴選Present和Dismiss動畫.gif

    來看代碼,在ViewController裏面有兩個按鈕,分別是PushSecondVCPresentThirdVCapi

- (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類來實現。bash

#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動畫便可。ui

- (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,來返回不一樣的動畫實現。atom

@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;
}
複製代碼

    看看實現效果 spa

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

相關文章
相關標籤/搜索