像 iOS10 音樂(lè)播放器一樣自定義轉(zhuǎn)場(chǎng)動(dòng)畫

蘋果在 iOS10 里重新設(shè)計(jì)了音樂(lè)播放器(Music),當(dāng)時(shí)在設(shè)計(jì)圈子還是個(gè)不小的事情。對(duì)于我個(gè)人而言,很喜歡它的「正在播放」界面的轉(zhuǎn)場(chǎng)方式。最近打算對(duì) OneShare 進(jìn)行最后的升級(jí),正好借鑒了這個(gè)轉(zhuǎn)場(chǎng)。發(fā)現(xiàn)步驟雖然不難,但還是有點(diǎn)繁瑣,有些細(xì)節(jié)需要注意一下。

在 iOS 中,自定轉(zhuǎn)場(chǎng)是通過(guò)實(shí)現(xiàn)協(xié)議 UIViewControllerTransitioningDelegate 來(lái)實(shí)現(xiàn)的。協(xié)議中提供了 TransitionAnimator(轉(zhuǎn)場(chǎng)動(dòng)畫)、InteractiveAnimator(轉(zhuǎn)場(chǎng)交互)和 PresentationController(視圖層級(jí)控制)相關(guān)的方法。

// TransitionAnimator(轉(zhuǎn)場(chǎng)動(dòng)畫)
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

// InteractiveAnimator(轉(zhuǎn)場(chǎng)交互)
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator;
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator;

// PresentationController(視圖層級(jí)控制)
- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source;

大多數(shù)情況下,我們只會(huì)用 TransitionAnimator 那兩個(gè)方法來(lái)實(shí)現(xiàn)轉(zhuǎn)場(chǎng)動(dòng)畫。再?gòu)?fù)雜一點(diǎn),我們希望實(shí)現(xiàn)自定手勢(shì)交互的時(shí)候,就需要重寫 InteractiveAnimator 那兩個(gè)方法了。分析蘋果的交互,會(huì)發(fā)現(xiàn)它還修改了視圖層級(jí)的關(guān)系。所以接下來(lái)模仿此交互的步驟大概就是:

  1. 實(shí)現(xiàn) UIViewControllerAnimatedTransitioning 自定義轉(zhuǎn)場(chǎng)動(dòng)畫;
  2. 實(shí)現(xiàn) UIPresentationController 自定義視圖控制器的層級(jí)表現(xiàn);
  3. 實(shí)現(xiàn) UIViewControllerInteractiveTransitioning 自定義交互方式。

所以首先在遵守 UIViewControllerTransitioningDelegate 協(xié)議的 AnimatorController 中,直接通過(guò) UIViewanimateWithDuration 方法去自定義轉(zhuǎn)場(chǎng)動(dòng)畫即可,這一步并沒(méi)什么難度。

接下來(lái)實(shí)現(xiàn)視圖層級(jí)控制之前,首先需要將被推出的視圖(這里是 ModalViewController)的 modalPresentationStyle 設(shè)置為自定義(UIModalPresentationCustom)。這樣才會(huì)調(diào)用協(xié)議中的 presentationControllerForPresentedViewController 方法。
實(shí)現(xiàn) UIPresentationController 時(shí),由于它同時(shí)接管了轉(zhuǎn)入(presentation)和轉(zhuǎn)出(dismissal),所以要在步驟 1 的基礎(chǔ)上增加一個(gè)轉(zhuǎn)出動(dòng)畫。這時(shí)可以另外新建一個(gè)類來(lái)處理轉(zhuǎn)出動(dòng)畫,也可以直接在上面那個(gè) animateTransition: 中實(shí)現(xiàn),我使用的時(shí)后者。

switch (self.transitionType) {

    case AnimatorDismiss: {
        [UIView animateWithDuration:[self transitionDuration:transitionContext]
                            animations:^{
                            // your animation
                            } completion:^(BOOL finished) {
                                [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
                            }];
        break;
    };

    case AnimatorPresent: {
        [[transitionContext containerView] addSubview:toViewController.view];
        // your variable
        [UIView animateWithDuration:[self transitionDuration:transitionContext] 
                            animations:^{
                            // your animation
                        }completion:^(BOOL finished) {
                            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
        break;
    };
}

最后就是實(shí)現(xiàn)交互了,因?yàn)槭怯?jì)劃打算使用下拉手勢(shì),所以自定義的 AnimatorInteractive 是繼承的 UIPercentDrivenInteractiveTransition,通過(guò)百分比來(lái)控制視圖的動(dòng)效。這里要注意在重寫協(xié)議中的 interactionControllerForDismissal 方法時(shí),不是通過(guò)手勢(shì)轉(zhuǎn)出的時(shí)候,要返回 nil ,否則協(xié)議不會(huì)去調(diào)用 animateTransition:

- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
    return self.dismissInteractor.interactionInProgress ? self.dismissInteractor : nil;
}

另外由于在轉(zhuǎn)場(chǎng)中使用了 CGAffineTransformMakeScale 轉(zhuǎn)入,所以在轉(zhuǎn)出時(shí),需要設(shè)置 toViewController.view.transform = CGAffineTransformIdentity 。這時(shí)候遇到了個(gè)麻煩,在使用下拉手勢(shì)的時(shí)候,父視圖在交互過(guò)程中,就已經(jīng)完成整個(gè)變化,導(dǎo)致取消下拉的時(shí)候,也不會(huì)復(fù)原。因?yàn)闆](méi)有找到解決辦法,仔細(xì)觀察了下蘋果的交互,發(fā)現(xiàn)也在下拉的時(shí)候父試圖也沒(méi)有動(dòng)效,所以猜測(cè)沒(méi)有直接解決辦法,最后就改成了音樂(lè)播放器這樣的實(shí)現(xiàn)方式

這里提一句在使用 Swift 實(shí)現(xiàn)的時(shí)候,iOS10 上通過(guò)下拉手勢(shì)關(guān)閉試圖沒(méi)成功的時(shí)候,ModalViewController 可能不會(huì)復(fù)原,這可能是蘋果在 iOS10 改了 UIView.animateTransition 實(shí)現(xiàn)的原因,用 UIViewPropertyAnimatorrunningPropertyAnimator 方法替代就可以了,在之前的一篇博客中提到過(guò)。

到這兒基本是就完了,還有最后一點(diǎn)修飾工作,就是 StatusBar 的樣式。其實(shí)設(shè)置起來(lái)不麻煩,在 ModalViewController 里如下設(shè)置即可:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.modalPresentationCapturesStatusBarAppearance = YES;
    // Do any additional setup after loading the view.
    ...
}

- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleLightContent;
}

但在實(shí)際操作中遇到了一點(diǎn)麻煩,因?yàn)椴恢朗裁磿r(shí)候把 Info.plist 里的 UIViewControllerBasedStatusBarAppearance 設(shè)置為 NO 了,所以一直沒(méi)有成功,最后看到這篇文章才意識(shí)到問(wèn)題。總結(jié)起來(lái)決定 StatusBar 樣式的優(yōu)先級(jí)依次是:

Info.plist > UINavigationController > Modal 中的 preferredStatusBarStyle

此文示例代碼:iOS10ModelPresent

參考鏈接:

Creating Custom UIViewController Transitions

Status bar colours: Everything there is to know

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容