iOS 自定義轉場動畫的那些事

iOS 7 以協議的方式開放了自定義轉場的 API,協議的好處是不再拘泥于具體的某個類,只要是遵守該協議的對象都能參與轉場,使其可以非常靈活的使用。轉場協議由5種協議組成,實際中只需要使用其中的兩個或三個便能實現絕大部分的轉場動畫。下面就簡單講解一下使用的心得體會,最后的總結你會發現很簡單。
參考: Custom Transitions Using View Controllers
喵神的博客

TransitionVC.gif

1 概念

  • 動畫控制器 (Animation Controllers) 遵守 UIViewControllerAnimatedTransitioning 協議,并且負責實際執行動畫。
  • 交互控制器 (Interaction Controllers) 通過遵守 UIViewControllerInteractiveTransitioning 協議,來控制可交互式(手勢或重力感應…)動畫轉場,大多都是使用它的一個子類UIPercentDrivenInteractiveTransition來更簡單的實現手勢交互動畫。
  • 轉場代理 (Transitioning Delegates) 根據不同的轉場類型,提供需要的動畫控制器和交互控制器。
    有3種轉場代理:
    UINavigationControllerDelegate –自定義navigationController轉場動畫的時候
    UITabBarControllerDelegate –自定義tabbarController轉場動畫的時候
    UIViewControllerTransitioningDelegate–自定義present/dismiss的時候
  • 轉場上下文 (Transitioning Context) 提供轉場中需要的數據,比如在轉場過程中所參與的視圖控制器和視圖的相關屬性。 轉場上下文對象遵守 UIViewControllerContextTransitioning 協議,并且這是由系統負責生成和提供的。
    轉場協調器(Transition Coordinators) 可以在運行轉場動畫時,并行的運行其他動畫。轉場協調器遵守UIViewControllerTransitionCoordinator 協議

2 自定義轉場動畫時你可能用到的那些方法

.
UIViewControllerContextTransitioning

這個接口用來提供切換的上下文給開發者使用,包含了從哪個VC到哪個VC等各類信息,一般不需要開發者自己實現。具體來說,iOS7的自定義切換目的之一就是切換相關代碼解耦,在進行VC切換時,做切換效果實現的時候必須需要 切換前后VC的一些信息,提供一些方法,以供我們使用。

-(UIView *)containerView; 
>VC切換所發生的view容器,開發者應該將切出的view移除,將切入的view加入到該view容器中。
-(UIViewController *)viewControllerForKey:(NSString *)key; 
>提供一個key,返回對應的VC。現在的SDK中key的選擇只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey兩種,分別表示將要切出和切入的VC。
 -(CGRect)initialFrameForViewController:(UIViewController *)vc; 
>某個VC的初始位置,可以用來做動畫的計算。
-(CGRect)finalFrameForViewController:(UIViewController *)vc;
> 與上面的方法對應,得到切換結束時某個VC應在的frame。
-(void)completeTransition:(BOOL)didComplete; 
>向這個context報告切換已經完成。

UIViewControllerAnimatedTransitioning

這個接口負責切換的具體內容,即“切換中應該發生什么”。開發者在做自定義切換效果時大部分代碼會是用來實現這個接口。它只有兩個方法需要我們實現:

-(NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext; 
>系統給出一個切換上下文,我們根據上下文環境返回這個切換所需要的花費時間(一般就返回動畫的時間就好了,系統會用這個時間來在百分比驅動的切換中進行幀的計算)。
-(void)animateTransition:(id < UIViewControllerContextTransitioning >)transitionContext; 
>在進行切換的時候將調用該方法,我們對于切換時的UIView的設置和動畫都在這個方法中完成。

UIViewControllerTransitioningDelegate

這個接口的作用比較簡單單一,在需要VC切換的時候系統會像實現了這個接口的對象詢問是否需要使用自定義的切換效果。這個接口共有四個類似的方法:

-(id< UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
>在 presented 動畫中會被調用
-(id< UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController *)dismissed;
>在 dismiss 動畫中會被調用
-(id< UIViewControllerInteractiveTransitioning >)interactionControllerForPresentation:(id < UIViewControllerAnimatedTransitioning >)animator;
-(id< UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id < UIViewControllerAnimatedTransitioning >)animator;

UIPercentDrivenInteractiveTransition
這是一個實現了UIViewControllerInteractiveTransitioning接口的類,為我們預先實現和提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。一般來說我們更多地會使用某些手勢來完成交互式的轉移,這樣使用這個類(一般是其子類,下面會講到)的話就會非常方便。我們在手勢識別中只需要告訴這個類的實例當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,十分方便。具體的幾個重要方法:

-(void)updateInteractiveTransition:(CGFloat)percentComplete
> 更新百分比,一般通過手勢識別的長度之類的來計算一個值,然后進行更新。之后的例子里會看到詳細的用法
-(void)cancelInteractiveTransition 
>報告交互取消,返回切換前的狀態
–(void)finishInteractiveTransition 
>報告交互完成,更新到切換后的狀態

UINavigationControllerDelegate

自定義navigationController轉場動畫的時候

- (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);
>視圖控制器轉換返回一個互動的動畫對象使用。

3 實戰

  • Present 動畫

就如上面所說轉場動畫遵循UIViewControllerAnimatedTransitioning協議拿到需要做動畫的視圖,首先建一個動畫類遵循這個協議。

@interface BouncePresentAnimation : NSObject <UIViewControllerAnimatedTransitioning>

遵循這個協議后實現里面的兩個方法
方法1:用來控制轉場動畫的時間

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

方法2:這里用來控制視圖切換做動畫,因為有 present 和 dismiss 所以在這里區分兩個動畫(需要一個type,后面會提到)
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
switch (self.type)
{
case TransitionTypePresent:
[self presentAnimation:transitionContext];
break;
case TransitionTypeDissmiss:
[self dismissAnimation:transitionContext];
break;
default:
break;
}
}
實現:具體也可以看 喵神的博客 此處為借鑒,旨在理解。

//實現present動畫邏輯代碼
     \\- (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 1. Get controllers from transition context
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    //fromVC.view.hidden = YES;

    UIView * screenSnapShotView = [fromVC.view snapshotViewAfterScreenUpdates:YES];

    // 2. Set init frame for toVC
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
    toVC.view.frame = CGRectOffset(finalFrame, 0, screenBounds.size.height);

    // 3. Add toVC's view to containerView
    UIView *containerView = [transitionContext containerView];

    [containerView insertSubview:screenSnapShotView aboveSubview:fromVC.view];

    [containerView addSubview:toVC.view];

    // 4. Do animate now
    NSTimeInterval duration = [self transitionDuration:transitionContext];

    [UIView animateWithDuration:duration
                          delay:0.0
         usingSpringWithDamping:0.6
          initialSpringVelocity:0.0
                        options:UIViewAnimationOptionTransitionFlipFromBottom
                     animations:^{
                         toVC.view.frame = finalFrame;
                     } completion:^(BOOL finished) {
                         // 5. Tell context that we completed.
                         [transitionContext completeTransition:YES];
                     }];

}
//實現dismiss動畫邏輯代碼
- (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
    
    // 1. Get controllers from transition context
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    // 2. Set init frame for fromVC
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGRect initFrame = [transitionContext initialFrameForViewController:fromVC];
    CGRect finalFrame = CGRectOffset(initFrame, 0, screenBounds.size.height);
    
    // 3. Add target view to the container, and move it to back.
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    [containerView sendSubviewToBack:toVC.view];
    
    // 4. Do animate now
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration animations:^{
        fromVC.view.frame = finalFrame;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

上面我們完成了動畫部分,接下來就是要告知系統,讓其去使用我們的動畫。就如上面提到的需要實現UIViewControllerTransitioningDelegate,在這里你會知道present和dismiss,所以在這里你可以傳個type告知你需要的是那種動畫,從而讓你的動畫類去實現相應動畫。
present 時會觸發的代理,這個時候告訴它我們需要的present動畫

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [BouncePresentAnimation transitionWithTransitionType:TransitionTypePresent];
}

dismiss 時會觸發的代理,這個時候告訴它我們需要的dismiss動畫

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [BouncePresentAnimation transitionWithTransitionType:TransitionTypeDissmiss];
}

當然前提是你遵從了它的代理,你可以讓前一個控制器作為后者的代理去實現這些方法。

 ModalViewController *mvc =  [[ModalViewController alloc] init];
 mvc.transitioningDelegate = self;
 mvc.delegate = self;
 [self presentViewController:mvc animated:YES completion:nil];
  • pop 動畫

和present一樣,如果想要自定義動畫,需要遵循UIViewControllerAnimatedTransitioning協議

@interface PushAnimation : NSObject<UIViewControllerAnimatedTransitioning>

同樣的實現協議里的兩個方法

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
    return 1.0f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    self.transitionContext = transitionContext;
    
    switch (self.type)
    {
        case TransitionAnimTypePop:
            [self transitionAnimTypePopWithTransitionContext:transitionContext];
            break;
        case TransitionAnimTypePush:
             [self transitionAnimTypePushWithTransitionContext:transitionContext];
            break;
        default:
            break;
    }
    
}

同樣的,你需要遵守 UINavigationControllerDelegate ,并在它的代理方法里面實現你要的動畫
添加代理

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.navigationController.delegate = self;
}

實現動畫

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    if (operation == UINavigationControllerOperationPush)
    {

        return [PushAnimation transitionWithTransitionType:
                TransitionAnimTypePush];
    }
    else if (operation == UINavigationControllerOperationPop)
    {
        return [PushAnimation transitionWithTransitionType:
                        TransitionAnimTypePop];

    }
    //返回nil則使用默認的動畫效果
    return nil;
}
  • 實現手勢動畫

如上所述,手勢動畫需要 UIPercentDrivenInteractiveTransition實現,提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。我們在手勢識別中只需要告訴這個類的實例當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,使動畫過渡的更加自然。
首先我們新建一個手勢處理類SwipeUpInteractiveTransition繼承于UIPercentDrivenInteractiveTransition這樣我們可以獲取相應的父類方法。
首先給所在的控制器的view添加手勢

[self.transitionController wireToViewController:mvc];

主要運用的手勢控制

- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
    switch (gestureRecognizer.state)
    {
        case UIGestureRecognizerStateBegan:
            // 1. Mark the interacting flag. Used when supplying it in delegate.
            self.interacting = YES;
            [self.presentingVC dismissViewControllerAnimated:YES completion:nil];
            break;
        case UIGestureRecognizerStateChanged:
        {
            // 2. Calculate the percentage of guesture
            CGFloat fraction = (translation.y / KWindowHeight);
            //Limit it between 0 and 1
            fraction = fminf(fmaxf(fraction, 0.0), 1.0);
            NSLog(@"fraction==%f",fraction);
            self.shouldComplete = (fraction > 0.5);
            [self updateInteractiveTransition:fraction];
            break;
        }
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
        {
            // 3. Gesture over. Check if the transition should happen or not
            self.interacting = NO;
            if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled)
            {
                [self cancelInteractiveTransition];
            }
            else
            {
                [self finishInteractiveTransition];
            }
            break;
        }
        default:
            break;
    }
}
  • 總結
    非交互動畫
    1.創建動畫類遵從UIViewControllerAnimatedTransitioning 實現里面的接口
    2.讓相應的控制器做代理
    3.present 實現 UIViewControllerTransitioningDelegate 接口,push 實現 UINavigationControllerDelegate 接口
    交互動畫
    1.創建動畫類繼承于 UIPercentDrivenInteractiveTransition ,然后在里面實現相應的動畫即可。
    2.在跳轉頁面時告訴動畫類你需要控制的頁面

注:在push動畫中的layer動畫可以任意替換為其他轉場動畫,只用把動畫加到 containerView.layer 上即可。CATransition 動畫傳送門

containerView.layer addAnimation:transition forKey:nil`

照例放Demo,僅供參考
Demo地址:
https://github.com/yongliangP/iOS-TransitionVC
如果你覺得對你有幫助請點喜歡哦,也可以關注我,每周至少一篇技術。
或者關注 我的專題 每周至少5篇更新,多謝支持哈。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • 前言 正如標題所示,iOS開發中, 自定義轉場的過渡動畫確實是必須要了解的, 在iOS7之后實現也是很簡單的. 如...
    ZeroJ閱讀 8,980評論 9 122
  • OC開發我們主要有以下三種自定義方法,供大家參考:Push & PopModalSegue 前兩種大家都很熟悉,第...
    ScaryMonsterLyn閱讀 1,671評論 1 3
  • 前言的前言 唐巧前輩在微信公眾號「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各項指標...
    VincentHK閱讀 5,399評論 3 44
  • 只有圓周 轉動許久之后 仍在徘徊 只因初心如磐 指針轉動 不同的角度 鐘點刻記 皆是過客罷 非一主擒之 此心終了 之后
    萍風衣舊閱讀 246評論 0 1
  • 有志者,事競成。下面是我這段時間經過深入思考后列出的一些人生計劃,希望自己從下周開始就調整狀態,堅持不懈地完成以下...
    紫丁香68閱讀 196評論 0 0