本文由我們團隊的王瑞華童鞋撰寫。
在iOS 7 釋出之後,UI上的兩個重要的變化是豐富的動畫使用和介面上各個方面對真實物理世界的模擬。然而互動式自定義過渡不是一個新特性,至少在iOS 3.2 中就已經存在了。例如,翻頁動畫就不僅是從一個頁面到另一個的過渡。它是一個互動式過渡——隨著手指移動的過渡。互動式自定義過渡是提升應用品質,使其在 App Store 大放異彩的重要工具。iOS 7 之後 SDK 允許自定義大部分過渡,包括檢視控制器的出現和消失、UINavigationController
的推入和淡出過渡、UITabBarController
的過渡,甚至是集合檢視的佈局變化過渡。
UICollectionView 的過渡動畫
在iOS 7 之後的日曆和照片應用當中,就運用集合檢視的過渡方式實現了一個viewController 向另一個 viewControler 的過渡。在 UICollectionViewController
中引入了 useLayoutToLayoutNavigationTransitions
這一屬性。當此屬性設定為 YES 時,在將 collecionViewController 推入導航控制器之前,推入過渡會使用 -setColletionViewLayout:animated:
來完成集合檢視佈局的變化。開發者要做的只是設定 useLayoutToLayoutNavigationTransitions = YES
,剩下的交給系統處理便可以。注意,該方法要求兩個 collectionView 擁有相同的資料。
自定義 viewController 過渡
實現 transition delegate
是 transition 動畫和自定義 presentation 的起點。該 transition delegate
就是開發者定義一個物件並遵循 UIViewControllerTransitioningDelegate
協議。下面看看該協議中包含什麼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// return 動畫物件,該動畫物件符合 UIViewControllerAnimatedTransitioning 協議,負責顯示 present 動畫。 - (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; // return 動畫物件,負責顯示 dismiss 動畫。 - (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed; // return 互動式動畫物件,該動畫符合 UIViewControllerInteractiveTransitioning 協議,採用觸控手勢或手勢識別器作為動畫的驅動,顯示 present 動畫。 - (nullable id )interactionControllerForPresentation:(id )animator; // return 互動式動畫,顯示 dismiss 動畫 - (nullable id )interactionControllerForDismissal:(id )animator; // return UIPresentationController,系統已經提供了各個演示樣式。 - (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0); |
示意圖如下:
解釋了這麼多,對過渡動畫需要遵循的 delegate
已經有了初步的瞭解,下面通過present 的方式實現一個 push 動畫。
- 我們首先建立兩個 viewController VC1 和 VC2,我們要實現 VC1 present 出 VC2,同時模擬 push 和 pop 動畫的效果。
- 然後我們需要建立出兩個管理過渡動畫的類,用於管理 present 動畫和 dismiss 動畫,兩個管理類大體實現相似。在
viewController
類中遵從UIViewControllerTransitioningDelegate
協議,實現協議方法。
以下是 present 動畫的例項,dismiss 動畫與之相似,不再贅述。
CustomPushAnimation.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@interface CustomPushAnimation : NSObject // 告訴系統動畫將花費的時間 - (NSTimeInterval)transitionDuration:(id)transitionContext { return 0.35; } // 執行實際的動畫 - (void)animateTransition:(id)transitionContext { // 目標 viewController UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect toFrame = [transitionContext finalFrameForViewController:toVC]; // 如果要對檢視做轉場動畫,檢視就必須要加入containerView中才能進行,可以理解containerView管理著所有做轉場動畫的檢視 UIView *containerView = [transitionContext containerView]; [containerView addSubview:toVC.view]; toVC.view.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, toVC.view.frame.size.width, toVC.view.frame.size.height); NSTimeInterval duration = [self transitionDuration:transitionContext]; [UIView animateWithDuration:duration animations:^{ toVC.view.frame = toFrame; } completion:^(BOOL finished) { // 通知系統的過渡動畫就完成了。你動畫完成後必須呼叫這個方法通知系統的過渡動畫就完成了。傳遞的引數必須顯示動畫是否成功完成。 [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; }]; } |
使用互動式自定義過渡
互動式過渡是由事件驅動的。可以是動作事件或者手勢,通常為手勢。要實現一個互動式過渡,除了需要跟之前相同的動畫,還需要告訴互動控制器動畫完成了多少。開發者只需要確定已經完成的百分比,其他交給系統去做就可以了。例如,(平移和縮放的距離 / 速度的量可以作為計算完成的百分比的引數)。
互動式控制器實現了 UIViewControllerInteractiveTransitioning
協議,該協議中包含如下方法:
1 |
- (void)startInteractiveTransition:(id )transitionContext; |
這個方法裡只能有一個動畫塊,動畫應該基於 UIView
而不是圖層,互動式過渡不支援 CATransition
或 CALayer
動畫。
互動式過渡的互動控制器應當是 UIPercentDrivenInteractiveTransition
子類。動畫類負責計算完成百分比,系統會自動更新動畫的中間狀態。
1 2 3 |
- (void)updateInteractiveTransition:(CGFloat)percentComplete; - (void)cancelInteractiveTransition; - (void)finishInteractiveTransition; |
根據手勢移動或者縮放的距離,計算出百分比並呼叫相應方法。
下面是簡單的互動式過渡的程式碼片段,該事例只做了互動式 dismiss 的部分,也只羅列了比較關鍵的部分。
SecondViewController.m
1 2 3 4 5 6 7 8 9 |
#pragma mark - UIViewControllerTransitioningDelegate - (id)animationControllerForDismissedController:(UIViewController *)dismissed { return self.dismissAnimator; } - (id)interactionControllerForDismissal:(id)animator { // 當切換完成或者取消的時候,記得把 return 設定為 nil。因為如果下一次的轉場是非互動的, 我們不應該返回這個舊的 interactionAnimator。 return self.interactiveAnimator.isInteractive? self.interactiveAnimator: nil; } |
CustomInteractiveTransition.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
- (instancetype)initWithViewController:(UIViewController *)viewController { if (self = [super init]) { _isInteractive = NO; _viewController = viewController; } return self; } - (void)panGestureAction:(UIPanGestureRecognizer *)recognizer { // 計算手勢距離與螢幕的比例,從而決定互動動畫的進度 CGFloat progress = [recognizer translationInView:self.viewController.view].y / (self.viewController.view.bounds.size.height * 1.0); progress = MIN(1.0, MAX(0.0, progress)); // 標記手勢互動狀態,為點選按鈕等非互動式 dismiss 動畫留出餘地 self.isInteractive = YES; if (recognizer.state == UIGestureRecognizerStateBegan) { [self.viewController dismissViewControllerAnimated:YES completion:nil]; } else if (recognizer.state == UIGestureRecognizerStateChanged) { [self updateInteractiveTransition:progress]; } else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) { if (progress > 0.5) { [self finishInteractiveTransition]; } else { [self cancelInteractiveTransition]; } } } |
UIViewControllerTransitionCoordinator 過渡協調器
所有的過渡都會建立一個過渡協調器,無論是否自定義。也就是說,當執行預設的模態過渡或push過渡時,也可以對檢視中的其他部分做動畫。
1 2 3 |
- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id context))animation completion:(void (^ __nullable)(id context))completion; - (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view animation:(void (^ __nullable)(id context))animation completion:(void (^ __nullable)(id context))completion; - (void)notifyWhenInteractionEndsUsingBlock: (void (^)(id context))handler; |
在 iOS中,可以取消一個過渡。這意味著,第二個檢視的 -viewWillApear
被呼叫,但 -viewDidApear
不一定被呼叫。如果程式碼寫的假定 -viewDidAppear
總是在 -viewWillAppear
之後執行則需要重新考慮邏輯實現。這種情況下UIViewControllerTransitionCoordinator
就有用了。在互動式過渡結束的時候,會在 block 中收到通知。