1. 程式人生 > >自定義modal動畫

自定義modal動畫

modal介面跳轉

在IOS中,相比大家都不陌生,除了導航控制器中的經常使用的push以外,我們也經常使用從下而上跳轉的控制器效果,這也就是modal效果。

modal跳轉使用

對於使用storyBoard內進行連線的跳轉,我們只需要將一個按鈕連線到我們需要的控制器,然後選擇modal效果,就可以實現從當前控制器跳轉到下一個控制器。

而如何通過程式碼的話,也十分容易,先準備好需要跳轉的modalVC控制器,只需要呼叫這麼一句話

[self presentViewController:modalVC animated:YES completion:completion];

然後當需要回到原先介面的時候,只需要在出發時間的方法內部呼叫

[self.dismissViewControllerAnimated:YES completion:nil];

modal擴充套件需求

一般而言,我們對於介面的跳轉也就到這裡為止了,所以我們可以看到目前大部分的APP在採用modal跳轉的時候都是很簡單的從下往上跳轉,然後回去從下往上。

但是當我們閒著蛋疼看一些酷炫的APP的時候,會發現有些介面的跳轉十分的酷炫。

  • 非常規跳轉動畫,也就是在modal的效果上,轉換成其他的一些轉場動畫,或者自己定義一些效果
  • 讓跳出來的介面不是全屏顯示,並且可以隨意的動畫跳出來,也就是類似下面的這樣的效果。這是我從GitHub Pod大神封裝的Pop動畫演示程式截圖的,
    https://github.com/schneiderandre/popping

    pop效果

modal系統預設情況下的呼叫原理

  • 對於StoryBoard拖線實現的跳轉來說,和其他控制器跳轉一樣,跳轉的前後的檢視控制器都是通過Segue物件來進行的。Segue會在跳轉前呼叫這個方法
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender

根據Segue物件的destinationViewController屬性可以獲得跳轉目標控制器,一般我們會該方法做跳轉前的一些動作。
* 而對於用程式碼跳轉的方式,因為是我們自己建立的目標控制器,因此,我們可以自己手動的在執行跳轉的presentViewController方法之前做好需要做的準備。
* 其實不論是StoryBoard還是程式碼,系統最終都會呼叫presentViewController進行跳轉。

內部系統提供的方法

  • 在modal跳轉的時候,其實每個需要被modal的檢視控制器都有兩個屬性:
    • modalPresentationStyle modal展現的型別。這是一個列舉型別,官方標頭檔案中表示了內部的全部型別:
typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
        UIModalPresentationFullScreen = 0,
        UIModalPresentationPageSheet NS_ENUM_AVAILABLE_IOS(3_2),
        UIModalPresentationFormSheet NS_ENUM_AVAILABLE_IOS(3_2),
        UIModalPresentationCurrentContext NS_ENUM_AVAILABLE_IOS(3_2),
        UIModalPresentationCustom NS_ENUM_AVAILABLE_IOS(7_0),
        UIModalPresentationOverFullScreen NS_ENUM_AVAILABLE_IOS(8_0),
        UIModalPresentationOverCurrentContext NS_ENUM_AVAILABLE_IOS(8_0),
        UIModalPresentationPopover NS_ENUM_AVAILABLE_IOS(8_0),
        UIModalPresentationNone NS_ENUM_AVAILABLE_IOS(7_0) = -1,         
};

從這些型別中,我們很容易就可以發現,其實系統本身也支援其他的一些展示的效果,不過效果有些侷限。但是IOS 7.0之後,我們發現這裡多了一個UIModalPresentationCustom。也就是說,我們可以自定義展現型別咯。
既然可以自定義,系統的其他型別就不管了,只要能實現自定義,那麼大千世界就任我為所欲為咯。

  • transitioningDelegate 動畫代理。 它是一個遵守了UIViewControllerTransitioningDelegate 協議的任意物件。那麼既然是協議,我們就先看看內部有哪些協議方法:
// 控制展現動畫的控制器,該方法要求返回遵守了UIViewControllerAnimatedTransitioning該協議的物件
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
// 控制消失動畫的控制器,該方法要求返回遵守了UIViewControllerAnimatedTransitioning該協議的物件
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
// 返回控制轉場動畫的控制器(UIPresentationController),該方法將正在顯示的物件,和需要顯示的物件都交給你,然後返回一個控制轉場動畫的動畫
- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

這個協議中還有另外兩個是支援實時互動的方法,這裡暫時涉及不到就先不提了。

從代理方法我們可以看出,根據上面的兩個方法,我們還需要了解

UIViewControllerAnimatedTransitioning這個協議
// 轉場動畫 (提供了轉場上下文,根據上下文處理轉場動畫)
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
// 轉場動畫時長,一般這裡都隨便填,真正的動畫時間看你自定義的動畫時長來決定
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;

自定義modal分析

根據以上的代理方法,根據系統的呼叫順序,合理的實現代理方法就可以進行自定義動畫了。

分析圖

根據分析圖可知,系統會根據系統內部順序呼叫代理方法,因此,我們需要繼承以上代理並且實現內部的代理方法。

專門設定一個動畫代理

為了防止主控制器的臃腫和方法的解耦,主控制不關心動畫的過程,而是我們專門建立一個物件來遵守所有的協議方法並且實現協議的方法。該代理遵守轉場代理和轉場動畫協議,所有的轉場動畫的協議方法全部通過他實現

每當我們需要進行跳轉的時候,我們只需要給需要跳轉的控制器的轉場代理和轉場方式,並且建立一個動畫代理實現代理方法,在轉場動畫的代理方法中實現不同的動畫即可根據自定義的方法進行跳轉了。

思路擴充套件

如果一個專案需要比較多的modal跳轉,或者需要酷炫的自定義modal跳轉。如果每次都要自己去根據上面的思路圖實現各種協議方法是一件十分費力的事情。
由於上面分解出來了一個動畫代理,因此該代理就可以進行封裝和重用。

封裝框架

目標:

控制器提供需要跳轉的控制器,提供跳轉控制器顯示的大小位置(frame),根據提供的顯示動畫和消失動畫即可實現自定義跳轉。

目的:

完全不關心內部代理方法的實現,系統呼叫的方式,只需要提供自定義的內容來完成自定義跳轉。

實現

做一個ViewController的分類,提供類似跳轉的方法。

/**
 *  自定義modal的跳轉方式
 *
 *  @param modalVC          需要展示的viewController
 *  @param presentFrame     展示檢視在螢幕的frame
 *  @param presentAnimation 展示動畫程式碼(返回的時間是轉場動畫上下文關閉的時間)
 *  @param dismissAnimation 消失動畫程式碼(返回的時間是轉場動畫上下文關閉的時間)
 關於轉場動畫上下文時長說明:
 轉場動畫上下文關閉的時間決定了改轉場動畫封鎖介面的使用者互動能力的時長,如果返回0表示立馬接受使用者互動,
 那麼可能存在在動畫過程中使用者互動而導致動畫達不到預期效果。
 一般建議返回動畫的時間長度,正好動畫結束,然後開啟使用者互動能力。
 特殊需求可以填寫特殊時長
 *  @param completion       完成回撥
 */
-(void)mk_presentViewController:(UIViewController *)modalVC
               withPresentFrame:(CGRect)presentFrame
           withPresentAnimation:(NSTimeInterval (^)(UIView *view))presentAnimation
           withDismissAnimation:(NSTimeInterval (^)(UIView *view))dismissAnimation
                 withCompletion:(void (^)(void))completion;

再通過執行時動態新增一個屬性用於儲存執行動畫的代理物件

const void *animationDelegateKey = "animationDelegate";
/**
 *  runtime動態載入執行動畫的代理屬性的set方法
 */
- (void)setAnimationDelegate:(ZJAnimationDelegate *)animationDelegate {
    objc_setAssociatedObject(self, animationDelegateKey, animationDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/**
 *  runtime動態載入執行動畫的代理屬性的get方法
 */
- (ZJAnimationDelegate *)animationDelegate {
    return objc_getAssociatedObject(self, animationDelegateKey);
}

代理物件內部儲存了跳轉需要的動畫block,在動畫方法內部在恰當的時間呼叫動畫。

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    if (self.isPresenting) {
        UIView *view = [transitionContext viewForKey:UITransitionContextToViewKey];
        [[transitionContext containerView] addSubview:view];
        NSTimeInterval time = self.presentAnimation(view);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [transitionContext completeTransition:YES];
        });
    }else {
        UIView *view = [transitionContext viewForKey:UITransitionContextFromViewKey];
        NSTimeInterval time = self.dismissAnimation(view);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [transitionContext completeTransition:YES];
            [view removeFromSuperview];
        });
    }
}

注意事項

[transitionContext completeTransition:YES];
一定要在動畫結束之後呼叫,不然系統任務轉場動畫沒結束,會一直封鎖整個介面和使用者教育的能力。

所以這句話我是採用使用者返回的時間來延遲載入。這句話只要在動畫開始之後執行都不影響動畫的執行效果,但是如果在動畫之前執行會提前開啟使用者互動能力,因此可能動畫會被使用者的其他操作而打斷。

GitHub地址

粗略的封裝了一個框架用於該需求。
pod上搜索 pod search ZJModalKing即可安裝。