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

由圖中可以看出要完成自定義轉場動畫,必須遵從 UIViewControllerAnimatedTransitioning
協議,協議中有兩個必須實現的方法一個是返回轉場時間,一個是具體轉場的實現。文章會結合5個最常用的動畫場景來說明轉場動畫。
先來看看網易嚴選App的轉場效果,可以看出當前頁面想要 Push
其他的頁面的時候,當前頁面會下沉同時其他頁面從右邊平移至左邊。 Present
頁面的時候,當前頁面也會下沉,目標檢視從底部彈出。


來看程式碼,在 ViewController
裡面有兩個按鈕,分別是 Push
出 SecondVC
和 Present
出 ThirdVC
。
- (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實現
Push
和 Pop
是相對的關係,所以在 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
要遵循 UINavigationBarDelegate
和 UIViewControllerTransitioningDelegate
,把 SecondVC
的 transitioningDelegate
設定為自己。然後根據不同的 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; } 複製程式碼
看看實現效果

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
,把 ThirdVC
的 transitioningDelegate
設定為自己,然後在代理方法中自定型別。
- (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
繼承自 UIPercentDrivenInteractiveTransition
, addGestureToViewController
是給目標控制器新增手勢。
#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
中,在 Present
出 ThirdVC
的時候新增手勢,在代理方法 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; } 複製程式碼
看看效果

小結
Push
、 Pop
、 Present
、 Dismiss
、手勢動畫都講解完了,可以看出,自定義轉場大致的步驟是
- 根據
viewForKey
來獲取轉場上下文 - 將要轉場的檢視加入轉場容器中
- 做出轉場動畫
- 標記轉場成功的狀態,根據狀態做相應的處理
理解了這些,再複雜的轉場動畫都能一步步分解出來,下面是格瓦拉App的轉場效果,第一次看的時候,覺得很酷炫,現在瞭解了轉場的核心後,覺得不那麼難了,有時間再把它的效果寫出來吧。

ofollow,noindex"> 原始碼:TransitionAnimation