你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 詳細講述iOS自定義轉場

詳細講述iOS自定義轉場

編輯:IOS開發基礎

70.jpg

本文由CocoaChina譯者@ALEX吳浩文翻譯
作者:Andrew Hershberger
原文:Custom Transitions on iOS


本文是iOS自定義視圖控制器轉場系列的第一篇。本文重點在於創建自定義動畫(非交互式)轉場。

當使用傳統的iOS應用程序時,我們經常在視圖間轉場。過去,如果你不想用標准的轉場動畫,全靠你自己,但在iOS 7中蘋果提供了一個新的API讓我們自定義這些動畫。

iOS提供了一些內置的轉場類型。Navigation controllers用push和pop來有層次地導航信息,tab bar controllers用切換tabs來在各部分之間跳轉,所有的視圖控制器可以根據特定任務模態化地present和dismiss另一個視圖控制器。

API介紹

  • 每一個自定義轉場涉及三個主要對象:

  • from view controller (消失的那個)

  • to view controller (出現的那個)

  • 一個動畫控制器

自定義轉場和在自定義之前一樣。對於push和pop,意味著調用UINavigationController的push-、pop-、或者set-方法來修改視圖控制器的堆棧。對於切換tabs,意味著修改UITabBarController的selectedIndex或selectedViewController屬性。對於modal,則意味著調用?[UIViewController presentViewController: animated: completion: ]或?[UIViewController dismissViewControllerAnimated: completion: ]。無論哪種情況,這個步驟都確定了“from view controller”和“to view controller”。

使用一個自定義轉場,你需要一個動畫控制器。對我來說這是自定義動畫轉場中最令人困惑的部分,因為每種轉場需要的動畫控制器不同。下表展示了如何為每種轉場提供動畫控制器。記著,委托方法總是返回動畫控制器。

65.png

動畫控制器可以是任何遵守UIViewControllerAnimatedTransitioning協議的對象。該協議聲明了兩個必須要實現的方法。一個提供了動畫的時間,另一個執行了動畫。這些方法調用時都傳遞一個上下文。上下文提供了入口來訪問信息和你創建自定義轉場需要的對象。以下是一些重點:

  • from view controller

  • to view controller

  • 兩個視圖控制器view的第一幀和最後一幀

  • container view,根據這篇文檔,“作為的轉場中視圖的父視圖”

重要:上下文還實現了-completeTransition:,你必須在你自定義轉場結束時調用一次。

這是關於自定義轉場所有你需要知道的。讓我們來看一些例子!

例子

所有這些例子都可以在GitHub找到,你可以克隆這些倉庫,然後邊往下看邊試試這些例子。

這三個例子都直接或子類化地使用了TWTExampleViewController。它只是設置了視圖的背景顏色,同時使你能夠通過點擊任何地方來結束例子回到主菜單。

輕彈push和pop

在這個例子中,目標是讓push和pop使用flip動畫而不是標准的slide動畫。一開始我建立一個navigation controller並把TWTPushExampleViewController的實例當作root。TWTPushExampleViewController添加了一個叫“Push”的右按鈕到導航欄。點擊它時,一個新的TWTPushExampleViewController的實例被壓入navigation的堆棧:

- (void)pushButtonTapped
{
    TWTPushExampleViewController *viewController = [[TWTPushExampleViewController alloc] init];
    viewController.delegate = self.delegate;
    [self.navigationController pushViewController:viewController animated:YES];
}

navigation controller的設置發生在TWTExamplesListViewController(運行demo主菜單的視圖控制器)。注意,它把自己置為navigation controller的委托:

- (void)presentPushExample
{
    TWTPushExampleViewController *viewController = [[TWTPushExampleViewController alloc] init];
    viewController.delegate = self;
 
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    navigationController.delegate = self;
 
    [self presentViewController:navigationController animated:YES completion:nil];
}

這意味著當navigation controller的轉場即將開始時,TWTExamplesListViewController將收到委托信息,並有機會返回一個動畫控制器。對於這種轉場,我使用一個TWTSimpleAnimationController的實例,它是一個+[UIView transitionFromView: toView: duration: options: completion:]的封裝:

- (id)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC
{
    TWTSimpleAnimationController *animationController = [[TWTSimpleAnimationController alloc] init];
    animationController.duration = 0.5;
    animationController.options = (  operation == UINavigationControllerOperationPush
                                   ? UIViewAnimationOptionTransitionFlipFromRight
                                   : UIViewAnimationOptionTransitionFlipFromLeft);
    return animationController;
}

如果轉場是一個push,我使用一個從右側的flip,否則,我使用一個從左側的flip。

以下是TWTSimpleAnimationController的實現:

- (NSTimeInterval)transitionDuration:(id)transitionContext
{
    return self.duration;
}
 
- (void)animateTransition:(id)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
    [toViewController.view layoutIfNeeded];
 
    [UIView transitionFromView:fromViewController.view
                        toView:toViewController.view
                      duration:self.duration
                       options:self.options
                    completion:^(BOOL finished) {
                        [transitionContext completeTransition:YES];
                    }];
}

記著,這兩個方法是UIViewControllerAnimatedTransitioning協議的一部分。在動畫控制器運行自定義轉場的時候,它們被UIKit調用。

這裡有一些關於animateTransition:需要注意的事情:

  • from view controller,to view controller,以及to view controller的最後一幀都從轉場的上下文中提取。其中還有一些其他可提取的信息,但在當前情況下,並不需要所有信息。

  • +[UIView transitionFromView: toView: duration: options: completion:]負責有層次地添加和刪除視圖。在後面的例子中,我將展示一種手動完成的情況。

  • 在轉場的completion代碼塊中,我調用[transitionContext completeTransition: YES]來告訴系統轉場結束了。如果你忘了這樣做,你將無法與app交互。如果出現這種情況,先檢查這個原因。

以上就是全部!現在有一些值得嘗試的東西:

  • 改變動畫的持續時間來看看它如何影響navigation bar的動畫。

  • 把動畫選項由flip改為page curls。

  • 找一個方法讓navigation的堆棧中的每個視圖控制器能指定自己的動畫控制器。看看在本文最後的推薦模式中提出的方法。

淡入淡出切換tabs

這個例子應該很熟悉。它使用和之前相同的觀點,但使用tab bar controller而不是navigation controller。

以下是TWTExamplesListViewController的設置:

- (void)presentTabsExample
{
    NSMutableArray *viewControllers = [[NSMutableArray alloc] init];
 
    for (NSUInteger i=0; i<3; i++) {
        TWTChangingTabsExampleViewController *viewController = [[TWTChangingTabsExampleViewController alloc] init];
        viewController.delegate = self;
        viewController.index = i;
        [viewControllers addObject:viewController];
    }
 
    UITabBarController *tabBarController = [[UITabBarController alloc] init];
    [tabBarController setViewControllers:viewControllers animated:NO];
    tabBarController.delegate = self;
 
    [self presentViewController:tabBarController animated:YES completion:nil];
}

TWTChangingTabsExampleViewController的index只是一個為每個視圖控制器自定義標簽的方法。就像之前一樣,TWTExamplesListViewController是委托,它將在切換tabs的時候提供動畫控制器:

- (id)tabBarController:(UITabBarController *)tabBarController
           animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                             toViewController:(UIViewController *)toVC
{
    TWTSimpleAnimationController *animationController = [[TWTSimpleAnimationController alloc] init];
    animationController.duration = 0.5;
    animationController.options = UIViewAnimationOptionTransitionCrossDissolve;
    return animationController;
}

看著熟悉嗎?這就夠了。

彈出一個覆蓋視圖

我最喜歡自定義轉場的一個用途,是它可以彈出覆蓋式的視圖控制器。以前,如果你想模仿一個社會化分享菜單,或任何需要呈現視圖控制器的同時保持背後內容的可見,你不得不從許多不幸的選項中做出選擇(直接添加視圖到窗口是我見過的最通用的方法)。然而幸運的是,這不再是必要的。

能這樣用的關鍵原因,是當被彈出視圖控制器的-modalPresentationStyle被置為UIModalPresentationCustom時,彈出視圖控制器就不會自動從視圖層中刪除。為了彈出一個覆蓋視圖,它僅僅如同是離開一般,這樣被彈出的視圖就可以顯示在它上方。

以下是TWTExamplesListViewController的設置:

- (void)presentPresentExample
{
    TWTOverlayExampleViewController *viewController = [[TWTOverlayExampleViewController alloc] init];
    viewController.delegate = self;
    viewController.modalPresentationStyle = UIModalPresentationCustom;
    viewController.transitioningDelegate = self;
 
    [self presentViewController:viewController animated:YES completion:nil];
}

這裡兩個要注意的事情是:modalPresentationStyle被置為UIModalPresentationCustom,被彈出視圖控制器的-transitioningDelegate被置為TWTExamplesListViewController。當轉場開始時,TWTExamplesListViewController收到轉場的委托信息:

- (id)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
    return [[TWTPresentAnimationController alloc] init];
}

如你所見,我創建了一個自定義動畫控制器類作為示例。我將要關注-animateTransition:的實現,因為它與之前的部分並不相同。

開始前,我用transitionContext提取toViewController(被彈出視圖控制器)和containerView(容器視圖)。

UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

UIView *containerView = [transitionContext containerView];

containerView在轉場中是from view controller和to view controller的父視圖。最初,to view controller的視圖沒有被添加到containerView,它的frame也未確定。我這裡直接賦值frame,你也可以使用auto layout。

CGRect frame = containerView.bounds;
frame = UIEdgeInsetsInsetRect(frame, UIEdgeInsetsMake(40.0, 40.0, 200.0, 40.0));
 
toViewController.view.frame = frame;
 
[containerView addSubview:toViewController.view];

對於這種轉場,我希望這個視圖pop到屏幕上。要做到這一點,我用UIView的spring動畫把視圖的scale從0.3變化到1。

說句題外話,若動畫中scale從大於0的值開始同時alpha從0到1,則可以讓你動畫速度比簡單的scale從0到1更快。試著刪除alpha動畫,再和從0開始的scale動畫比較。

toViewController.view.alpha = 0.0;
toViewController.view.transform = CGAffineTransformMakeScale(0.3, 0.3);
 
NSTimeInterval duration = [self transitionDuration:transitionContext];
 
[UIView animateWithDuration:duration / 2.0 animations:^{
    toViewController.view.alpha = 1.0;
}];
 
CGFloat damping = 0.55;
 
[UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:damping initialSpringVelocity:1.0 / damping options:0 animations:^{
    toViewController.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
    [transitionContext completeTransition:YES];
}];

在completion塊,我調用-completeTransition:,到這裡就完了!現在到了彈回...

在彈回開始時,TWTExamplesListViewController收到轉場的委托信息:

- (id)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[TWTDismissAnimationController alloc] init];
}

返回另一個自定義動畫控制器。讓我們看一看-animateTransition::

UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
 
NSTimeInterval duration = [self transitionDuration:transitionContext];
 
[UIView animateWithDuration:3.0 * duration / 4.0
                      delay:duration / 4.0
                    options:UIViewAnimationOptionCurveEaseIn
                 animations:^{
                     fromViewController.view.alpha = 0.0;
                 }
                 completion:^(BOOL finished) {
                     [fromViewController.view removeFromSuperview];
                     [transitionContext completeTransition:YES];
                 }];
 
[UIView animateWithDuration:2.0 * duration
                      delay:0.0
     usingSpringWithDamping:1.0
      initialSpringVelocity:-15.0
                    options:0
                 animations:^{
                     fromViewController.view.transform = CGAffineTransformMakeScale(0.3, 0.3);
                 }
                 completion:nil];

如你所見,這個想法是倒轉彈出動畫。需要注意的重要區別是,from view controller的視圖在轉場結束前就從視圖層中被刪除了。

在繼續之前,試著改變一下彈回的動畫。

推薦模式

到這裡,您可能已經注意到這個API在很大程度上依賴於協議和委托方法。因此很容易把自定義轉場編寫得混亂且無法重用。這裡有一些模式幫助我保持整潔:

1.用專門的對象作動畫控制器

雖然一個視圖控制器可以直接代碼量翻倍當作動畫控制器,但幾乎可以肯定這樣做會減少自定義轉場的可重用性。此外,視圖控制器已經因為做得太多而臭名昭著。你可以創建可重用的動畫控制器,只需創建NSObject子類,遵守UIViewControllerAnimatedTransitioning協議,然後在需要的時候返回它們的實例。

我們在Toast裡的TWTSimpleAnimationController就是這樣做的,它很容易重用和用CocoaPod集成。

2.不要讓UINavigationControllerDelegate需要知道轉場的細節

用navigation controller的委托返回動畫控制器是好,如果你想讓所有的push和pop轉場都以同樣的方式進行。然而在某些情況下,你可能希望只自定義一個push或pop。

一種混亂的方式是把navigation controller的委托對象暫時改變成一個知道這種轉場的對象。另一種混亂的方式是使navigation controlle的委托根據特定的from view controllers和to view controllers有邏輯地去確定。顯然,這些都不是好的選擇。

用模式可以干淨利落地解決這個問題,只要向UIViewController添加-pushAnimationController和-popAnimationController屬性。然後navigation controller的委托就可以返回由被push的動畫控制器和被pop視圖控制器指定的動畫控制器。這使得navigation controller的委托保持通用同時避免了委托對象的改變。TWTNavigationControllerDelegate實現了這種模式,它也同時包括在Toast裡。

這樣就結束了這個創建自定義動畫視圖控制器轉場的介紹。一定要看看我們的在Toast裡的視圖控制器轉場模塊,並繼續關注第二部分,該部分將包括自定義交互式轉場。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved