1. 程式人生 > >iOS自定義轉場動畫

iOS自定義轉場動畫

http://www.jianshu.com/p/45434f73019e

更新,更簡單的自定義轉場整合!

寫在前面

這兩天閒下來好好的研究了一下自定義轉場,關於這方面的文章網路上已經很多了,作為新手,我想通過這篇文章把自己這幾天的相關學習心得記錄一下,方便加深印響和以後的回顧,這是我第一寫技術文章,不好之處請諒解,通過這幾天的學習,我嘗試實現了四個效果,廢話不多說,先上效果圖:

DEMO ONE:一個彈性的present動畫,支援手勢present和dismiss


彈性pop

DEMO TWO:一個類似於KeyNote的神奇移動效果push動畫,支援手勢pop


神奇移動

DEMO THREE:一個翻頁push效果,支援手勢PUSH和POP


翻頁效果

DEMO FOUR:一個小圓點擴散present效果,支援手勢dimiss


擴散效果

動手前

大家都知道從iOS7開始,蘋果就提供了自定義轉場的API,模態推送present和dismiss、導航控制器push和pop、標籤控制器的控制器切換都可以自定義轉場了,關於過多的理論我就不太多說明了,大家可以先參照onevcat大神的這篇部落格:WWDC 2013 Session筆記 - iOS7中的ViewController切換,我想把整個自定義轉場的步驟做個總結:

  1. 我們需要自定義一個遵循的<UIViewControllerAnimatedTransitioning>協議的動畫過渡管理物件,並實現兩個必須實現的方法:

     //返回動畫事件  
     - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
     //所有的過渡動畫事務都在這個方法裡面完成
     - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
  2. 我們還需要自定義一個繼承於UIPercentDrivenInteractiveTransition的手勢過渡管理物件,我把它成為百分比手勢過渡管理物件,因為動畫的過程是通過百分比控制的
  3. 成為相應的代理,實現相應的代理方法,返回我們前兩步自定義的物件就OK了 !

    模態推送需要實現如下4個代理方法,iOS8新的那個方法我暫時還沒有發現它的用處,所以暫不討論

     //返回一個管理prenent動畫過渡的物件
     - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
     //返回一個管理pop動畫過渡的物件
     - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
     //返回一個管理prenent手勢過渡的物件
     - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
     //返回一個管理pop動畫過渡的物件
     - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

    導航控制器實現如下2個代理方法

     //返回轉場動畫過渡管理物件
     - (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                       interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
     //返回手勢過渡管理物件
     - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                animationControllerForOperation:(UINavigationControllerOperation)operation
                                             fromViewController:(UIViewController *)fromVC
                                               toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

    標籤控制器也有相應的兩個方法

     //返回轉場動畫過渡管理物件
     - (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
                   interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController NS_AVAILABLE_IOS(7_0);
     //返回手勢過渡管理物件
     - (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
         animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                           toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);
  4. 如果看著這些常常的代理方法名頭疼的話,沒關係,先在demo中用起來吧,慢慢就習慣了,其實哪種自定義轉場都只需要這3個步驟,如果不需要手勢控制,步驟2還可以取消,現在就讓我們動手來實現效果吧

動手吧!

demo one

1、我們首先建立2個控制器,為了方便我稱做present操作的為vc1、被present的為vc2,點選一個控制器上的按鈕可以push出另一個控制器
2、 然後我們建立一個過渡動畫管理的類,遵循<UIViewControllerAnimatedTransitioning>協議,我這裡是XWPresentOneTransition,由於我們要同時管理present和dismiss2個動畫,你可以實現相應的兩個類分別管理兩個動畫,但是我覺得用一個類來管理就好了,看著比較舒服,邏輯也比較緊密,因為present和dismiss的動畫邏輯很類似,寫在一起,可以相互參考,所以我定義了一個列舉和兩個初始化方法:

    XWPresentOneTransition.h

    typedef NS_ENUM(NSUInteger, XWPresentOneTransitionType) {
        XWPresentOneTransitionTypePresent = 0,//管理present動畫
        XWPresentOneTransitionTypeDismiss//管理dismiss動畫
    };

    @interface XWPresentOneTransition : NSObject<UIViewControllerAnimatedTransitioning>
    //根據定義的列舉初始化的兩個方法
    + (instancetype)transitionWithTransitionType:(XWPresentOneTransitionType)type;
    - (instancetype)initWithTransitionType:(XWPresentOneTransitionType)type;

3、 然後再.m檔案裡面實現必須實現的兩個代理方法

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

    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
        //為了將兩種動畫的邏輯分開,變得更加清晰,我們分開書寫邏輯,
        switch (_type) {
            case XWPresentOneTransitionTypePresent:
                [self presentAnimation:transitionContext];
                break;

            case XWPresentOneTransitionTypeDismiss:
                [self dismissAnimation:transitionContext];
                break;
        }
    }
    //實現present動畫邏輯程式碼
    - (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
    }
    //實現dismiss動畫邏輯程式碼
    - (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{

    }

4、 設定vc2的transitioningDelegate,我就設為它自己咯,我實在vc2的init方法中設定的,並實現代理方法

    - (instancetype)init
    {
        self = [super init];
        if (self) {
            self.transitioningDelegate = self;
            //為什麼要設定為Custom,在最後說明.
            self.modalPresentationStyle = UIModalPresentationCustom;
        }
        return self;
    }

    - (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
        //這裡我們初始化presentType
        return [XWPresentOneTransition transitionWithTransitionType:XWPresentOneTransitionTypePresent];
    }
    - (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
        //這裡我們初始化dismissType
        return [XWPresentOneTransition transitionWithTransitionType:XWPresentOneTransitionTypeDismiss];
    }

5、 至此我們所有的準備工作就做好了,下面只需要專心在presentAnimation:方法和dismissAnimation方法中實現動畫邏輯就OK了,先看presentAnimation:

        - (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{`
        //通過viewControllerForKey取出轉場前後的兩個控制器,這裡toVC就是vc1、fromVC就是vc2
        UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
        //snapshotViewAfterScreenUpdates可以對某個檢視截圖,我們採用對這個截圖做動畫代替直接對vc1做動畫,因為在手勢過渡中直接使用vc1動畫會和手勢有衝突,    如果不需要實現手勢的話,就可以不是用截圖檢視了,大家可以自行嘗試一下
        UIView *tempView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
        tempView.frame = fromVC.view.frame;
        //因為對截圖做動畫,vc1就可以隱藏了
        fromVC.view.hidden = YES;
        //這裡有個重要的概念containerView,如果要對檢視做轉場動畫,檢視就必須要加入containerView中才能進行,可以理解containerView管理著所有做轉場動畫的檢視
        UIView *containerView = [transitionContext containerView];
        //將截圖檢視和vc2的view都加入ContainerView中
        [containerView addSubview:tempView];
        [containerView addSubview:toVC.view];
        //設定vc2的frame,因為這裡vc2present出來不是全屏,且初始的時候在底部,如果不設定frame的話預設就是整個螢幕咯,這裡containerView的frame就是整個螢幕
        toVC.view.frame = CGRectMake(0, containerView.height, containerView.width, 400);
        //開始動畫吧,使用產生彈簧效果的動畫API
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1.0 / 0.55 options:0 animations:^{
            //首先我們讓vc2向上移動
            toVC.view.transform = CGAffineTransformMakeTranslation(0, -400);
            //然後讓截圖檢視縮小一點即可
            tempView.transform = CGAffineTransformMakeScale(0.85, 0.85);
        } completion:^(BOOL finished) {
            //使用如下程式碼標記整個轉場過程是否正常完成[transitionContext transitionWasCancelled]代表手勢是否取消了,如果取消了就傳NO表示轉場失敗,反之亦然,如果不用手勢present的話直接傳YES也是可以的,但是無論如何我們都必須標記轉場的狀態,系統才知道處理轉場後的操作,否者認為你一直還在轉場中,會出現無法互動的情況,切記!
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            //轉場失敗後的處理
            if ([transitionContext transitionWasCancelled]) {
                //失敗後,我們要把vc1顯示出來
                fromVC.view.hidden = NO;
                //然後移除截圖檢視,因為下次觸發present會重新截圖
                [tempView removeFromSuperview];
            }
            }];
        }

再看dismissAnimation 方法

        - (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
        //注意在dismiss的時候fromVC就是vc2了,toVC才是VC1了,注意這個關係
        UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
        UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        //參照present動畫的邏輯,present成功後,containerView的最後一個子檢視就是截圖檢視,我們將其取出準備動畫
        UIView *tempView = [transitionContext containerView].subviews[0];
        //動畫吧
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            //因為present的時候都是使用的transform,這裡的動畫只需要將transform恢復就可以了
            fromVC.view.transform = CGAffineTransformIdentity;
            tempView.transform = CGAffineTransformIdentity;
        } completion:^(BOOL finished) {
            if ([transitionContext transitionWasCancelled]) {
                //失敗了標記失敗
                [transitionContext completeTransition:NO];
            }else{
                //如果成功了,我們需要標記成功,同時讓vc1顯示出來,然後移除截圖檢視,
                [transitionContext completeTransition:YES];
                toVC.view.hidden = NO;
                [tempView removeFromSuperview];
            }
            }];
        }

6、如果不需要手勢控制,這個轉場就算完成了,下面我們來新增手勢,首先建立一個手勢過渡管理的類,我這裡是XWInteractiveTransition,因為無論哪一種轉場,手勢控制的實質都是一樣的,我乾脆就把這個手勢過渡管理的類封裝了一下,具體可以在.h檔案裡面檢視,在接下來的三個轉場效果中我們都可以便捷的是使用它 .m檔案說明

        //通過這個方法給控制器的View新增相應的手勢
        - (void)addPanGestureForViewController:(UIViewController *)viewController{
        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
        //將傳入的控制器儲存,因為要利用它觸發轉場操作
        self.vc = viewController;
        [viewController.view addGestureRecognizer:pan];    
        }  

        //關鍵的手勢過渡的過程
        - (void)handleGesture:(UIPanGestureRecognizer *)panGesture{
        //persent是根據panGesture的移動距離獲取的,這裡就不說明了,可具體去程式碼中檢視
        switch (panGesture.state) {
        case UIGestureRecognizerStateBegan:
            //手勢開始的時候標記手勢狀態,並開始相應的事件,它的作用在使用這個類的時候說明
            self.interation = YES;
            //手勢開始是觸發對應的轉場操作,方法程式碼在後面
            [self startGesture];
            break;
        case UIGestureRecognizerStateChanged:{
            //手勢過程中,通過updateInteractiveTransition設定轉場過程進行的百分比,然後系統會根據百分比自動佈局控制元件,不用我們控制了
            [self updateInteractiveTransition:persent];
            break;
        }
        case UIGestureRecognizerStateEnded:{
            //手勢完成後結束標記並且判斷移動距離是否過半,過則finishInteractiveTransition完成轉場操作,否者取消轉場操作,轉場失敗
            self.interation = NO;
            if (persent > 0.5) {
                [self finishInteractiveTransition];
            }else{
                [self cancelInteractiveTransition];
            }
            break;
        }
        default:
            break;
            }
        }  
        //觸發對應轉場操作的程式碼如下,根據type(type是我自定義的列舉值)我們去判斷是觸發哪種操作,對於push和present由於要傳入需要push和present的控制器,為了解耦,我用block把這個操作交個控制器去做了,讓這個手勢過渡管理者可以充分被複用
        - (void)startGesture{
        switch (_type) {
        case XWInteractiveTransitionTypePresent:{
            if (_presentConifg) {
                _presentConifg();
            }
        }
            break;

        case XWInteractiveTransitionTypeDismiss:
            [_vc dismissViewControllerAnimated:YES completion:nil];
            break;
        case XWInteractiveTransitionTypePush:{
            if (_pushConifg) {
                _pushConifg();
            }
        }
            break;
        case XWInteractiveTransitionTypePop:
            [_vc.navigationController popViewControllerAnimated:YES];
            break;
            }
        }

7、 手勢過渡管理者就算完畢了,這個手勢管理者可以用到其他任何的模態和導航控制器轉場中,以後都不用在寫了,現在把他用起來,在vc2和vc1中建立相應的手勢過渡管理者,並放到相應的代理方法去返回它

        //建立dismiss手勢過渡管理者,present的手勢過渡要在vc1中建立,因為present的手勢是載入vc1的view上的,我選擇通過代理吧vc1中建立的手勢過渡管理者傳過來
        self.interactiveDismiss = [XWInteractiveTransition interactiveTransitionWithTransitionType:XWInteractiveTransitionTypeDismiss             GestureDirection:XWInteractiveTransitionGestureDirectionDown];
            [self.interactiveDismiss addPanGestureForViewController:self];
            [_interactivePush addPanGestureForViewController:self.navigationController];
            //返回dissmiss的手勢過渡管理
        - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:        (id<UIViewControllerAnimatedTransitioning>)animator{
            //在沒有用手勢觸發的dismiss的時候需要傳nil,否者無法點選dimiss,所以interation就是用來判斷是否是手勢觸發轉場的
            return _interactiveDismiss.interation ? _interactiveDismiss : nil;
        }

        //返回present的手勢管理,這個手勢管理者是在vc1中建立的,我用代理傳過來的
        - (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:        (id<UIViewControllerAnimatedTransitioning>)animator{
            XWInteractiveTransition *interactivePresent = [_delegate interactiveTransitionForPresent];
            return interactivePresent.interation ? interactivePresent : nil;
        }

8、 終於完成了,再來看一下效果,是不是還不錯!


彈性pop

DEMO TWO

1、 建立動畫過渡管理者的程式碼就不重複說明了,我仿造demo1,利用列舉建立了一個同時管理push和pop的管理者,然後動畫的邏輯程式碼集中在doPushAnimationdoPopAnimation中,很多內容都在demo1中說明了,下面的註釋就比較簡單了,來看看

        //Push動畫邏輯
        - (void)doPushAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
        XWMagicMoveController *fromVC = (XWMagicMoveController *)[transitionContext     viewControllerForKey:UITransitionContextFromViewControllerKey];
        XWMagicMovePushController *toVC = (XWMagicMovePushController *)[transitionContext     viewControllerForKey:UITransitionContextToViewControllerKey];
        //拿到當前點選的cell的imageView
        XWMagicMoveCell *cell = (XWMagicMoveCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.currentIndexPath];
        UIView *containerView = [transitionContext containerView];
        //snapshotViewAfterScreenUpdates 對cell的imageView截圖儲存成另一個檢視用於過渡,並將檢視轉換到當前控制器的座標
        UIView *tempView = [cell.imageView snapshotViewAfterScreenUpdates:NO];
        tempView.frame = [cell.imageView convertRect:cell.imageView.bounds toView: containerView];
        //設定動畫前的各個控制元件的狀態
        cell.imageView.hidden = YES;
        toVC.view.alpha = 0;
        toVC.imageView.hidden = YES;
        //tempView 新增到containerView中,要保證在最前方,所以後新增
        [containerView addSubview:toVC.view];
        [containerView addSubview:tempView];
        //開始做動畫
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.55     initialSpringVelocity:1 / 0.55 options:0 animations:^{
            tempView.frame = [toVC.imageView convertRect:toVC.imageView.bounds toView:containerView];
            toVC.view.alpha = 1;
        } completion:^(BOOL finished) {
            //tempView先隱藏不銷燬,pop的時候還會用
            tempView.hidden = YES;
            toVC.imageView.hidden = NO;
            //如果動畫過渡取消了就標記不完成,否則才完成,這裡可以直接寫YES,如果有手勢過渡才需要判斷,必須標記,否則系統不會中動畫完成的部署,會出現無法互動之類的bug
            [transitionContext completeTransition:YES];
            }];
        }  

    //Pop動畫邏輯
        - (void)doPopAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
        XWMagicMovePushController *fromVC = (XWMagicMovePushController *)[transitionContext     viewControllerForKey:UITransitionContextFromViewControllerKey];
        XWMagicMoveController *toVC = (XWMagicMoveController *)[transitionContext     viewControllerForKey:UITransitionContextToViewControllerKey];
        XWMagicMoveCell *cell = (XWMagicMoveCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.currentIndexPath];
        UIView *containerView = [transitionContext containerView];
        //這裡的lastView就是push時候初始化的那個tempView
        UIView *tempView = containerView.subviews.lastObject;
        //設定初始狀態
        cell.imageView.hidden = YES;
        fromVC.imageView.hidden = YES;
        tempView.hidden = NO;
        [containerView insertSubview:toVC.view atIndex:0];
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.55     initialSpringVelocity:1 / 0.55 options:0 animations:^{
            tempView.frame = [cell.imageView convertRect:cell.imageView.bounds toView:containerView];
            fromVC.view.alpha = 0;
        } completion:^(BOOL finished) {
            //由於加入了手勢必須判斷
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            if ([transitionContext transitionWasCancelled]) {//手勢取消了,原來隱藏的imageView要顯示出來
                //失敗了隱藏tempView,顯示fromVC.imageView
                tempView.hidden = YES;
                fromVC.imageView.hidden = NO;
            }else{//手勢成功,cell的imageView也要顯示出來
                //成功了移除tempView,下一次pop的時候又要建立,然後顯示cell的imageView
                cell.imageView.hidden = NO;
                [tempView removeFromSuperview];
            }
          }];
        }

2、 然後將這個動畫過渡管理者和demo1中建立的手勢過渡管理者分別放到正確的代理方法中,用起來就可以了


神奇移動

DEMO THREE

1、 直接看看doPushAnimationdoPopAnimation的動畫邏輯,這次使用了CAGradientLayer給動畫的過程增加了陰影

    //Push動畫邏輯
    - (void)doPushAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    //還是使用截圖大法來完成動畫,不然還是會有奇妙的bug;
    UIView *tempView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
    tempView.frame = fromVC.view.frame;
    UIView *containerView = [transitionContext containerView];
    //將將要動畫的檢視加入containerView
    [containerView addSubview:toVC.view];
    [containerView addSubview:tempView];
    fromVC.view.hidden = YES;
    [containerView insertSubview:toVC.view atIndex:0];
    //設定AnchorPoint,並增加3D透視效果
    [tempView setAnchorPointTo:CGPointMake(0, 0.5)];
    CATransform3D transfrom3d = CATransform3DIdentity;
    transfrom3d.m34 = -0.002;
    containerView.layer.sublayerTransform = transfrom3d;
    //增加陰影
    CAGradientLayer *fromGradient = [CAGradientLayer layer];
    fromGradient.frame = fromVC.view.bounds;
    fromGradient.colors = @[(id)[UIColor blackColor].CGColor,
                        (id)[UIColor blackColor].CGColor];
    fromGradient.startPoint = CGPointMake(0.0, 0.5);
    fromGradient.endPoint = CGPointMake(0.8, 0.5);
    UIView *fromShadow = [[UIView alloc]initWithFrame:fromVC.view.bounds];
    fromShadow.backgroundColor = [UIColor clearColor];
    [fromShadow.layer insertSublayer:fromGradient atIndex:1];
    fromShadow.alpha = 0.0;
    [tempView addSubview:fromShadow];
    CAGradientLayer *toGradient = [CAGradientLayer layer];
    toGradient.frame = fromVC.view.bounds;
    toGradient.colors = @[(id)[UIColor blackColor].CGColor,
                            (id)[UIColor blackColor].CGColor];
    toGradient.startPoint = CGPointMake(0.0, 0.5);
    toGradient.endPoint = CGPointMake(0.8, 0.5);
    UIView *toShadow = [[UIView alloc]initWithFrame:fromVC.view.bounds];
    toShadow.backgroundColor = [UIColor clearColor];
    [toShadow.layer insertSublayer:toGradient atIndex:1];
    toShadow.alpha = 1.0;
    [toVC.view addSubview:toShadow];
    //動畫吧
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        //翻轉截圖檢視
        tempView.layer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 1, 0);
        //給陰影效果動畫
        fromShadow.alpha = 1.0;
        toShadow.alpha = 0.0;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        if ([transitionContext transitionWasCancelled]) {
            //失敗後記得移除截圖,下次push又會建立
            [tempView removeFromSuperview];
            fromVC.view.hidden = NO;
        }
    }];
}}
    //Pop動畫邏輯
    - (void)doPopAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = [transitionContext containerView];
    //拿到push時候的的截圖檢視
    UIView *tempView = containerView.subviews.lastObject;
    [containerView addSubview:toVC.view];
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
           //把截圖檢視翻轉回來
        tempView.layer.transform = CATransform3DIdentity;
        fromVC.view.subviews.lastObject.alpha = 1.0;
        tempView.subviews.lastObject.alpha = 0.0;
    } completion:^(BOOL finished) {
        if ([transitionContext transitionWasCancelled]) {
            [transitionContext completeTransition:NO];
        }else{
            [transitionContext completeTransition:YES];
            [tempView removeFromSuperview];
            toVC.view.hidden = NO;
        }
    }];}

2、 最後用上去在加上手勢就是這個樣子啦


翻頁效果

DEMO FOUR

1、 直接看看doPresentAnimationdoDismissAnimation的動畫邏輯,這次使用了CASharpLayer和UIBezierPath

    //Present動畫邏輯
    - (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    //拿到控制器獲取button的frame來設定動畫的開始結束的路徑
    UINavigationController *fromVC = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    XWCircleSpreadController *temp = fromVC.viewControllers.lastObject;
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    //畫兩個圓路徑
    UIBezierPath *startCycle =  [UIBezierPath bezierPathWithOvalInRect:temp.buttonFrame];
    //通過如下方法計算獲取在x和y方向按鈕距離邊緣的最大值,然後利用勾股定理即可算出最大半徑
    CGFloat x = MAX(temp.buttonFrame.origin.x, containerView.frame.size.width - temp.buttonFrame.origin.x);
    CGFloat y = MAX(temp.buttonFrame.origin.y, containerView.frame.size.height - temp.buttonFrame.origin.y);
    //勾股定理計算半徑
    CGFloat radius = sqrtf(pow(x, 2) + pow(y, 2));
    //以按鈕中心為圓心,按鈕中心到螢幕邊緣的最大距離為半徑,得到轉場後的path
    UIBezierPath *endCycle = [UIBezierPath bezierPathWithArcCenter:containerView.center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
    //建立CAShapeLayer進行遮蓋
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    //設定layer的path保證動畫後layer不會回彈
    maskLayer.path = endCycle.CGPath;
    //將maskLayer作為toVC.View的遮蓋
    toVC.view.layer.mask = maskLayer;
    //建立路徑動畫
    CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    maskLayerAnimation.delegate = self;
    //動畫是加到layer上的,所以必須為CGPath,再將CGPath橋接為OC物件
    maskLayerAnimation.fromValue = (__bridge id)(startCycle.CGPath);
    maskLayerAnimation.toValue = (__bridge id)((endCycle.CGPath));
    maskLayerAnimation.duration = [self transitionDuration:transitionContext];
    maskLayerAnimation.delegate = self;
    //設定淡入淡出
    maskLayerAnimation.timingFunction = [CAMediaTimingFunction  functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [maskLayerAnimation setValue:transitionContext forKey:@"transitionContext"];
    [maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
}

    //Dismiss動畫邏輯
    - (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext{
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UINavigationController *toVC = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    XWCircleSpreadController *temp = toVC.viewControllers.lastObject;
    UIView *containerView = [transitionContext containerView];
    //畫兩個圓路徑
    CGFloat radius = sqrtf(containerView.frame.size.height * containerView.frame.size.height + containerView.frame.size.width * containerView.frame.size.width) / 2;
    UIBezierPath *startCycle = [UIBezierPath bezierPathWithArcCenter:containerView.center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
    UIBezierPath *endCycle =  [UIBezierPath bezierPathWithOvalInRect:temp.buttonFrame];
    //建立CAShapeLayer進行遮蓋
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.fillColor = [UIColor greenColor].CGColor;
    maskLayer.path = endCycle.CGPath;
    fromVC.view.layer.mask = maskLayer;
    //建立路徑動畫
    CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    maskLayerAnimation.delegate = self;
    maskLayerAnimation.fromValue = (__bridge id)(startCycle.CGPath);
    maskLayerAnimation.toValue = (__bridge id)((endCycle.CGPath));
    maskLayerAnimation.duration = [self transitionDuration:transitionContext];
    maskLayerAnimation.delegate = self;
    maskLayerAnimation.timingFunction = [CAMediaTimingFunction  functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [maskLayerAnimation setValue:transitionContext forKey:@"transitionContext"];
    [maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
}

2、最後在animationDidStop的代理方法中處理到動畫的完成邏輯,處理方式都類似

    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    switch (_type) {
        case XWCircleSpreadTransitionTypePresent:{
            id<UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:@"transitionContext"];
            [transitionContext completeTransition:YES];
            [transitionContext viewControllerForKey:UITransitionContextToViewKey].view.layer.mask = nil;
        }
            break;
        case XWCircleSpreadTransitionTypeDismiss:{
            id<UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:@"transitionContext"];
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            if ([transitionContext transitionWasCancelled]) {
                [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
            }
        }
            break;
    }
}

3、 最後用上去在加上手勢就是這個樣子啦


擴散效果

總結

1、關於:self.modalPresentationStyle = UIModalPresentationCustom;我查看了檢視層級後發現,如果使用了Custom,在present動畫完成的時候,presentingView也就是demo one中的vc1的view會從containerView中移除,只是移除,並未銷燬,此時還被持有著(dismiss後還得回來呢!),如果設定custom,那麼present完成後,它一直都在containerView中,只是在最後面,所以需不需要設定custom可以看動畫完成後的情況,是否還需要看見presentingViewController,但是記住如果沒有設定custom,在disMiss的動畫邏輯中,要把它加回containerView中,不然就不在咯~!
2、感覺寫了好多東西,其實只要弄懂了轉場的邏輯,其實就只需要寫動畫的邏輯就行了,其他東西都是固定的,而且蘋果提供的這種控制轉場的方式可充分解耦,除了寫的手勢過渡管理可以拿到任何地方使用,所有的動畫過渡管理者都可以很輕鬆的複用到其他轉場中,都不用分是何種轉場,demo沒有寫標籤控制器的轉場,實現方法也是完全類似的,大家可以嘗試一下,四個demo的github地址:自定義轉場動畫demo



文/wazrx(簡書作者)
原文連結:http://www.jianshu.com/p/45434f73019e
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。