iOS視圖控制器轉場動畫

屏幕左邊緣右滑返回,TabBar 滑動切換,你是否喜歡並十分依賴這兩個操做,甚至以爲 App 不支持這類操做的話簡直反人類?這兩個操做在大屏時代極大提高了操做效率,其背後的技術即是今天的主題:視圖控制器轉換(View Controller Transition)。ios

前言

經過學習seedanteiOS 視圖控制器轉場詳解:從入門到精通的這篇文章,對視圖轉場有了新的認識,寫這篇文章的目的,主要是記錄一下本身對視圖轉場動畫的理解並作一個總結方便之後查閱。git

目前爲止,官方支持如下幾種方式的自定義轉場:github

  1. UINavigationController 中 push 和 pop
  2. UITabBarController 中切換 Tab
  3. Modal 轉場:presentation 和 dismissal,俗稱視圖控制器的模態顯示和消失,僅限於modalPresentationStyle屬性爲 UIModalPresentationFullScreen 或 UIModalPresentationCustom這兩種模式
  4. UICollectionViewController 的佈局轉場:僅限於 UICollectionViewController 與 UINavigationController 結合的轉場方式

轉場協議

轉場動畫的本質: 下一場景(子 VC)的視圖替換當前場景(子 VC)的視圖以及相應的控制器(子 VC)的替換,表現爲當前視圖消失和下一視圖出現。 iOS 7 以協議的方式開放了自定義轉場的 API,協議的好處是再也不拘泥於具體的某個類,只要是遵照該協議的對象都能參與轉場,很是靈活。主要有一下幾個協議:bash

  1. 轉場代理(Transition Delegate)
  2. 動畫控制器(Animation Controller)
  3. 交互控制器(Interaction Controller)
  4. 轉場環境(Transition Context)
  5. 轉場協調器(Transition Coordinator)

對於非交互式動畫咱們只須要實現轉場代理動畫控制器協議便可,對於交互式動畫咱們還須要實現交互控制器協議。👇下面對每一個協議進行詳細介紹。ide

1. 轉場代理

UINavigationControllerDelegate

/*返回已經實現的`動畫控制器`,若是返回nil則使用系統默認的動畫效果*/
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation  fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);


/*返回已經實現的`交互控制器`,若是返回nil則不支持手勢交互*/
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
複製代碼

UITabBarControllerDelegate

一樣做爲容器控制器,UITabBarController 的轉場代理和 UINavigationController 相似,經過相似的方法提供動畫控制器,不過UINavigationControllerDelegate的代理方法裏提供了操做類型,但UITabBarControllerDelegate的代理方法沒有提供滑動的方向信息,須要咱們來獲取滑動的方向。佈局

/*同理返回已經實現的`動畫控制器`,返回nil是默認效果*/
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController animationControllerForTransitionFromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC;

/*返回已經實現的`交互控制器`,返回nil則不支持用戶交互*/
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController NS_AVAILABLE_IOS(7_0);
複製代碼

UIViewControllerTransitioningDelegate

Modal 轉場的代理協議UIViewControllerTransitioningDelegate是 iOS 7 新增的,其爲 presentation 和 dismissal 轉場分別提供了動畫控制器。 UIPresentationController只在 iOS 8中可用,經過available關鍵字能夠解決 API 的版本差別。學習

/*present時調用,返回已經實現的`動畫控制器`*/
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

/*dissmis時調用,返回已經實現的`動畫控制器`*/
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

/*交互動畫present時調用,返回已經實現的`交互控制器`*/
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;

/*交互動畫dissmiss時調用,返回已經實現的`交互控制器`*/
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

/*ios8新增的協議*/
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);
複製代碼

Modal 轉場的代理由 presentedVC 的transitioningDelegate屬性來提供,這與前兩種容器控制器的轉場不同,另外,須要將 presentedVC 的modalPresentationStyle屬性設置爲.Custom或.FullScreen,只有這兩種模式下才支持自定義轉場,該屬性默認值爲.FullScreen。當與 UIPresentationController 配合時該屬性必須爲.Custom動畫

2. 動畫控制器

動畫控制器負責添加視圖以及執行動畫,遵照UIViewControllerAnimatedTransitioning協議,該協議要求實現如下方法:ui

/*返回動畫執行時間,通常0.5s就足夠了*/
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;

/*核心方法,作一些動畫相關的操做*/
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
複製代碼

UIKit 在轉場開始前生成遵照轉場環境協議<UIViewControllerContextTransitioning>的對象 transitionContext,它有如下幾個方法來提供動畫控制器須要的信息:spa

/*獲取容器視圖,轉場發生的地方*/
UIView *containerView = [transitionContext containerView];

/*獲取參與轉場的視圖控制器*/
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

/*獲取參與參與轉場的視圖View*/
UIView *fromView;
UIView *toView;
 if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
      //iOS8新增的方法
      fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
      toView = [transitionContext viewForKey:UITransitionContextToViewKey];
  }else{
      //iOS8以前的方法
      fromView = fromVC.view;
      toView = toVC.view;
  }
複製代碼

經過viewForKey:獲取的視圖是viewControllerForKey:返回的控制器的根視圖,或者 nil。viewForKey:方法返回 nil 只有一種狀況: UIModalPresentationCustom 模式下的 Modal 轉場 ,經過此方法獲取 presentingView 時獲得的將是 nil,所以在 Modal 轉場中,較穩妥的方法是從 fromVC 和 toVC 中獲取 fromViewtoView

須要注意的地方:

  1. 將 toView 添加到容器視圖中,使得 toView 在屏幕上顯示(Modal 轉場中此點稍有不一樣)沒必要非得是addSubview:,某些場合你可能須要調整 fromView 和 toView 的顯示順序,總之將之加入到containerView 裏就好了。
  2. 動畫結束後正確地結束轉場過程。轉場的結果有兩種:完成或取消。非交互轉場的結果只有完成一種狀況,不過交互式轉場須要考慮取消的狀況。如何結束取決於轉場的進度,經過transitionWasCancelled()方法來獲取轉場的結果,而後使用completeTransition:來通知系統轉場過程結束。
  3. 轉場結束後,fromView 會從視圖結構中移除,UIKit 自動替咱們作了這事,你也能夠手動處理提早將 fromView 移除,這徹底取決於你。
  4. Model中,在 Custom 模式下的 dismissal轉場中不要像其餘的轉場那樣將 toView(presentingView) 加入 containerView,不然presentingView將消失不見,而應用則也極可能假死。而 FullScreen 模式下可使用與前面的容器類 VC 轉場一樣的代碼,(Modal 轉場在 Custom 模式下必須區分 presentation 和 dismissal 轉場,而在 FullScreen 模式下能夠不用這麼作)。

特殊的Model轉場

iOS8引入了UIPresentationController類,該類接管了 UIViewController 的顯示過程,爲其提供轉場和視圖管理支持,model模式必須是CustomUIPresentationController類主要給 Modal 轉場帶來了如下幾點變化:

  1. 定製 presentedView 的外觀:設定 presentedView 的尺寸以及在 containerView 中添加自定義視圖併爲這些視圖添加動畫。
  2. 能夠選擇是否移除 presentingView。
  3. 能夠在不須要動畫控制器的狀況下單獨工做。
  4. iOS 8 中的適應性佈局

👇介紹相關的方法:

/**在呈現過渡即將開始的時候被調用的*/
- (void)presentationTransitionWillBegin;

/**在呈現過渡結束時被調用的*/
- (void)presentationTransitionDidEnd:(BOOL)completed;

/**在退出過渡即將開始的時候被調用的*/
- (void)dismissalTransitionWillBegin;

/**在退出的過渡結束時被調用的*/
- (void)dismissalTransitionDidEnd:(BOOL)completed;

/*提供給動畫控制器使用的視圖,默認返回 presentedVC.view,經過重寫該方法返回其餘視圖,但必定要是 presentedVC.view 的上層視圖。對 presentedView 的外觀進行定製。*/
- (UIView *)presentedView;

/*返回動畫結束後的`presented view`的frame*/
- (CGRect)frameOfPresentedViewInContainerView;
複製代碼

有個問題,沒法直接訪問動畫控制器,不知道轉場的持續時間,怎麼與轉場過程同步?這時候前面提到的用處甚少的轉場協調器(Transition Coordinator)將在這裏派上用場。該對象可經過UIViewControllertransitionCoordinator()方法獲取,這是 iOS 7 爲自定義轉場新增的 API,該方法只在控制器處於轉場過程當中才返回一個與當前轉場有關的有效對象,其餘時候返回 nil。 轉場協調器遵照<UIViewControllerTransitionCoordinator>協議,它含有如下幾個方法:

/*與動畫控制器中的轉場動畫同步,執行其餘動畫*/
- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;

/*與動畫控制器中的轉場動畫同步,在指定的視圖內執行動畫*/
- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view animation:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
複製代碼

在 iOS 7 中,Custom 模式的 Modal 轉場裏,presentingView不會被移除,若是咱們要移除它並妥善恢復會破壞動畫控制器的獨立性使得第三方動畫控制器沒法直接使用;在 iOS 8 中,UIPresentationController解決了這點,給予了咱們選擇的權力,經過重寫下面的方法來決定 presentingView是否在 presentation 轉場結束後被移除:

- (BOOL)shouldRemovePresentersView
複製代碼

返回 true 時,presentation 結束後presentingView被移除,在 dimissal 結束後 UIKit 會自動將 presentingView 恢復到原來的視圖結構中。此時,Custom 模式與 FullScreen 模式下無異,徹底沒必要理會前面 dismissal 轉場部分的差別了

3. 交互控制器

實現交互效果須要在非交互轉場的基礎上實現下面兩個方法:

  1. 由轉場代理提供交互控制器,這是一個遵照<UIViewControllerInteractiveTransitioning>協議的對象,不過系統已經打包好了現成的類UIPercentDrivenInteractiveTransition供咱們使用。咱們不須要作任何配置,僅僅在轉場代理的相應方法中提供一個該類實例便能工做。另外交互控制器必須有動畫控制器才能工做。
  2. 交互控制器還須要交互手段的配合,最多見的是使用手勢,或是其餘事件,來驅動整個轉場進程。
/*更新轉場進度,進度數值範圍爲0.0~1.0。*/
- (void)updateInteractiveTransition:(CGFloat)percentComplete;

/*取消轉場,轉場動畫從當前狀態返回至轉場發生前的狀態。*/
- (void)cancelInteractiveTransition;

/*完成轉場,轉場動畫從當前狀態繼續直至結束。*/
- (void)finishInteractiveTransition;
複製代碼

交互控制協議<UIViewControllerInteractiveTransitioning>只有一個必須實現的方法:

/*交互轉場,獲取轉場上下文*/
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
複製代碼

須要注意的地方: 若是在轉場代理中提供了交互控制器,而轉場發生時並無方法來驅動轉場進程(好比手勢),轉場過程將一直處於開始階段沒法結束,應用界面也會失去響應:在 NavigationController 中點擊 NavigationBar 也能實現 pop 返回操做,但此時沒有了交互手段的支持,轉場過程卡殼;在 TabBarController 的代理裏提供交互控制器存在一樣的問題,點擊 TabBar 切換頁面時也沒有實現交互控制。所以僅在確實處於交互狀態時才提供交互控制器,可使用一個變量來標記交互狀態,該變量由交互手勢來更新狀態。

- (void)leftPan:(UIScreenEdgePanGestureRecognizer *)recognizer{
    CGPoint currentPoint = [recognizer translationInView:recognizer.view];
    CGFloat progress = currentPoint.x/CGRectGetWidth(recognizer.view.frame);
    progress = MIN(1, MAX(0, progress));
    if (recognizer.state == UIGestureRecognizerStateBegan){
     //使用變量來標記交互狀態
        _isStart = YES;
        [self.controller.navigationController popViewControllerAnimated:YES];
        
    }else if (recognizer.state == UIGestureRecognizerStateChanged){
        [self updateInteractiveTransition:progress];
        
    }else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled){
        _isStart = NO;
        if (progress > 0.4) {
            [self finishInteractiveTransition];
        }else{
            [self cancelInteractiveTransition];
        }
    }
}
複製代碼
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                   interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{
    
    return _percentModel.isStart ? _percentModel : nil;
}
複製代碼

動畫實例

1. Keynote中的神奇移動效果

KeyNoteTransition.gif

實現思路: 獲取UICollectionView當前選中的Cell上的ImageView,而且對ImageView進行截圖,將ToView和截圖ImageView添加到ContainerView,以動畫的方式將截圖imageView的frame轉換爲toView的ImageView的Frame。下面請看Push詳細代碼,Pop代碼同理:

- (void)PushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext{
    /*切出和切入的VC*/
    FistViewController *fromVC = (FistViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    DetailController *toVC = (DetailController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    /*VC切換所發生的view容器,開發者應該將切出的view移除,將切入的view加入到該view容器中。*/
    UIView *containerView = [transitionContext containerView];
    
    /*對選中cell的imageView截圖*/
    NSIndexPath *indexPath = [[fromVC.myCollection indexPathsForSelectedItems] firstObject];
    fromVC.selectIndexPath = indexPath;
    FirstCollectionViewCell *selectCell = (FirstCollectionViewCell*)[fromVC.myCollection cellForItemAtIndexPath:indexPath];
    UIView *snapShotView = [selectCell.avatarimageView snapshotViewAfterScreenUpdates:NO];
    
    // 將rect從view中轉換到當前視圖中,返回在當前視圖中的rect
    snapShotView.frame = fromVC.finalCellRect = [containerView convertRect:selectCell.avatarimageView.frame fromView:selectCell.avatarimageView.superview];
    selectCell.avatarimageView.hidden = YES;
    
    
    /*設置第二個控制器的位置,透明度*/
    toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
    toVC.view.alpha = 0;
    toVC.avatarImageView.hidden = YES;
    
    CGPoint currentCenter = toVC.textView.center;
    toVC.textView.center = CGPointMake(currentCenter.x + 30, currentCenter.y);
    
    /*將動畫先後的兩個View添加到containerView,注意添加順序,snapShotView在上面*/
    [containerView addSubview:toVC.view];
    [containerView addSubview:snapShotView];
    
    /*開始動畫*/
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
        
        //textView中心點
        toVC.textView.center = currentCenter;
        
        //透明度,frame變換
        toVC.view.alpha = 1.0;
        snapShotView.frame = [containerView convertRect:toVC.avatarImageView.frame toView:toVC.avatarImageView.superview];
    } completion:^(BOOL finished) {
        toVC.avatarImageView.hidden = NO;
        selectCell.avatarimageView.hidden = NO;
        [snapShotView removeFromSuperview];
        
        /*告訴系統動畫結束*/
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }];
}
複製代碼

2. Mask圓形轉場

MaskTransition.gif
實現思路: 使用View的layer的遮罩效果,Layer遮罩是一個圓形,push變換時圓形的半徑從button的半徑增長到button圓心距屏幕邊緣的最大值,pop時相反,push動畫代碼以下,pop動畫同理:

- (void)pushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext{
    //獲取fromVC和toVC,以及containerView
    FirstViewController *fromVC = (FirstViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    SecondViewController *toVC = (SecondViewController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = [transitionContext containerView];
    
    //設置遮罩
    CGPoint buttonCenter = fromVC.targetButton.center;
    CGRect buttonFrame = fromVC.targetButton.frame;
    CGFloat paddingX = MAX(buttonCenter.x, CGRectGetWidth(fromVC.view.frame) - buttonCenter.x);
    CGFloat paddingY = MAX(buttonCenter.y, CGRectGetHeight(fromVC.view.frame) - buttonCenter.y);

    CGFloat distance = sqrtf((paddingX * paddingX) + (paddingY * paddingY));
    UIBezierPath *startPath = [UIBezierPath bezierPathWithOvalInRect:buttonFrame];
    UIBezierPath *endPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(buttonFrame, -(distance - CGRectGetWidth(buttonFrame)/2.0), -(distance - CGRectGetHeight(buttonFrame)/2.0))];
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    
    //將參與變換的視圖添加到contaier上
    [containerView addSubview:toVC.view];
    toVC.view.layer.mask = maskLayer;
    //防止最後閃屏一下
    maskLayer.path = endPath.CGPath;
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
    animation.duration = 0.6;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    animation.delegate = self;
    animation.fromValue = (__bridge id)startPath.CGPath;
    animation.toValue = (__bridge id)endPath.CGPath;
    [animation setValue:@"maskAnimation" forKey:AnimationKey];
    [animation setValue:transitionContext forKey:TransitionContextKey];
    [maskLayer addAnimation:animation forKey:nil];
}
複製代碼

動畫結束後要將toView或者FromView的遮罩設置爲nil。

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    if ([[anim valueForKey:AnimationKey] isEqualToString:@"maskAnimation"]){
        id <UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:TransitionContextKey];
        SecondViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        toVC.view.layer.mask = nil;
        
        //完成動畫
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
        
    }else if ([[anim valueForKey:AnimationKey]isEqualToString:@"maskAnimationPop"]){
        id <UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:TransitionContextKey];
        SecondViewController *FromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
        FromVC.view.layer.mask = nil;
        
        //完成動畫
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }
}
複製代碼

3. Presentation轉場動畫

presentation.gif
這個動畫使用iOS8引入了 UIPresentationController類。原理上面已經解釋的很清楚了,直接上代碼:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    /*獲取controller,ContainerView*/
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = [transitionContext containerView];
    UIView *fromView;
    UIView *toView;
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
        fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    }else{
        fromView = fromVC.view;
        toView = toVC.view;
    }
    
    CGRect fromViewFrame = [transitionContext initialFrameForViewController:fromVC];
    CGRect toViewFrame = [transitionContext finalFrameForViewController:toVC];
    
    /*進行動畫*/
    if (_type == AnimationTypePresent) {
        CGRect orginalFrame = CGRectZero;
        orginalFrame.origin = CGPointMake(CGRectGetMinX(containerView.bounds), CGRectGetMaxY(containerView.bounds));
        orginalFrame.size = toViewFrame.size;
        toView.frame = orginalFrame;
        [containerView addSubview:toView];
    }else if (_type == AnimationTypeDissmiss){   
        /**
         處理 Dismissal 轉場,按照上一小節的結論,.Custom模式下不要將 toView添加到 containerView
         */
        fromViewFrame = CGRectOffset(fromViewFrame, 0, CGRectGetHeight(containerView.bounds));
    }
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        if (_type == AnimationTypePresent) {
            toView.frame = toViewFrame;
        }else if (_type == AnimationTypeDissmiss){
            fromView.frame = fromViewFrame;
        }
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
    
}
複製代碼

動畫控制協調器中執行背景透明度變化,與動畫控制器中的轉場動畫同步。

self.dimmingView.alpha = 0.0;
    [self.presentingViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
        self.dimmingView.alpha = 0.7;
        self.presentingViewController.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.92, 0.92);
    } completion:nil];
複製代碼

資料

  1. 文中Demo下載
  2. 開源視圖控制器的轉場庫**VCTransitionsLibrary**
  3. 一些好看的轉場動畫效果GitHub
相關文章
相關標籤/搜索