本文學習下自定義ViewController的切換,從無交互的到交互式切換。html
(本文已同步到個人小站:icocoa,歡迎訪問。)ios
iOS7中定義了3個協議:git
UIViewControllerTransitioningDelegate:
用於支持自定義切換或切換交互,定義了一組供animator對象實現的協議,來自定義切換。
能夠爲動畫的三個階段單獨提供animator對象:presenting,dismissing,interacting。github
UIViewControllerAnimatedTransitioning:
主要用於定義切換時的動畫。這個動畫的運行時間是固定的,並且沒法進行交互。app
UIViewControllerInteractiveTransitioning:
負責交互動畫的對象。
該對象是經過加快/減慢動畫切換的過程,來響應觸發事件或者隨時間變化的程序輸入。對象也能夠提升切換的逆過程來響應變化。
好比iOS7上NavController響應手指滑動來切換viewController
若是要提供交互,那麼也須要提供實現UIViewControllerAnimatedTransitioning的對象,這個對象能夠就是以前實現UIViewControllerInteractiveTransitioning的對象,也能夠不是。
若是不須要(動畫按預先設置的進行),則能夠本身實現。若是要提供交互,那麼也須要實現UIViewControllerAnimatedTransitioning。iview
上述是API文檔中的說明,咱們按圖索驥,根聽說明一步一步來實現一個無交互的切換動畫。ide
爲了方便,我在一個viewController A裏添加按鈕,點擊後以present modal的方式跳轉到viewController B。B中也放置一個按鈕,用來回到A。
爲了支持自定義transition,iOS7中UIViewController多了transitioningDelegate的屬性。這個delegate須要實現相關的protocol,能夠是viewcontroller自己。不過,這樣的話,很顯然不利於自定義部分的重用。所以咱們新建一個類:學習
@interface ZJTransitionDelegateObj : NSObject<UIViewControllerTransitioningDelegate>
@end
而後實現delegate,UIViewControllerTransitioningDelegate定義了4個protocol,後2個是用於交互時用的,這裏咱們只需實現前2個。動畫
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; - (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed; - (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator; - (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
前2個返回的是實現 UIViewControllerAnimatedTransitioning 協議的對象,這裏咱們返回self,這樣意味着咱們的ZJTransitionDelegateObj類還須要實現相應的協議:ui
// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to // synchronize with the main animation. - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext; // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext; @optional // This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked. - (void)animationEnded:(BOOL) transitionCompleted;
根聽說明,咱們能夠看到主要是實現第2個協議。transitionContext是一個實現UIViewControllerContextTransitioning協議的對象,再進一步查看該協議,能夠看到一系列方法,具體的就不詳細展開,看一下代碼:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext; { UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *containView = [transitionContext containerView]; [containView addSubview:toViewController.view]; CGRect rect = toViewController.view.frame; rect.origin.x = -320; rect.origin.y = -rect.size.height; toViewController.view.frame = rect; [UIView animateKeyframesWithDuration:1.5 delay:0 options:UIViewKeyframeAnimationOptionLayoutSubviews animations:^{ CGRect frame = rect; frame.origin.x = 0; frame.origin.y = 0; toViewController.view.frame = frame; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; }
內容很簡單,這裏須要注意的是 [transitionContext completeTransition:YES] 很重要。若是沒有使用,系統會不知道當前的transition是否已經結束,這樣形成的後果:使app進入某種未知狀態,好比presentingViewController能看到新view可是沒法和用戶交互。關於這一點,Apple把它放置在頭文件裏說明了,因此我推薦你們遇到問題的時候,不妨先直接查看頭文件中的註釋說明(xCode中按住command後鼠標點擊類名)。
接下來,看一下app,發現present的方式是以對角的方式出現了。若是你不當心點擊了ViewCOntroller B的dismiss按鈕,發現以前的view也以一樣的方式出現了。這是由於咱們還沒有作present和dismiss的區分。接下來給ZJTransitionDelegateObj增長增長Bool屬性
@interface ZJTransitionDelegateObj ()
@property (nonatomic) BOOL isPresent;
@end
並在協議中賦值:
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; { self.isPresent = YES; return self; } - (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed; { self.isPresent = NO; return self; }
而後修改動畫:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext; { UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *fromViewController = [transitionContext viewControllerForKey: UITransitionContextFromViewControllerKey]; UIView *containView = [transitionContext containerView]; CGRect rect = toViewController.view.frame; if (self.isPresent) { [containView addSubview:toViewController.view]; rect.origin.x = - rect.size.width; rect.origin.y = - rect.size.height; toViewController.view.frame = rect; [UIView animateKeyframesWithDuration:1.5 delay:0 options:UIViewKeyframeAnimationOptionLayoutSubviews animations:^{ CGRect frame = rect; frame.origin.x = 0; frame.origin.y = 0; toViewController.view.frame = frame; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } else { [containView insertSubview:toViewController.view atIndex:0]; rect = fromViewController.view.frame; [UIView animateKeyframesWithDuration:1.5 delay:0 options:UIViewKeyframeAnimationOptionLayoutSubviews animations:^{ CGRect frame = rect; frame.origin.x = - rect.size.width; frame.origin.y = - rect.size.height; fromViewController.view.frame = frame; } completion:^(BOOL finished) { [fromViewController.view removeFromSuperview]; [transitionContext completeTransition:YES]; }]; } }
假設A present B,那麼fromViewController和toViewController在present和dismiss是正好相反的,如圖:
並且present時,container view中沒有subview,須要本身添加B的view。而dismiss的時候,container view中已經添加了B的view,因此要先把A的view添加到最底層,而後對B的view作動畫,最後還要把它移除。
這樣,一個簡單的custom transition 就已經完成了。
下面,咱們趁熱打鐵,來實現一個交互式的custom transion。何謂交互式的custom transion呢?舉個簡單的例子,有個navController,push了viewController A,在A頁面能夠經過手指從左向右的滑動的方式pop到上一級ViewController。在滑動的過程當中,你也能夠取消當前的pop。這種交互的方式,是Apple在iOS7中推薦的。
咱們看一下WWDC中的講義,來領會一下這樣的一個過程:
上圖就是交互式動畫過程當中的狀態變化,其中更新,結束和取消的幾個狀態,是須要客戶端調用來通知系統的。
根據WWDC的說明,最簡單的實現交互式動畫的方法就是經過繼承 UIPercentDrivenInteractiveTransition。
下面咱們嘗試實現一個交互式動畫,我選擇的是對nav的pop添加交互式動畫,經過兩個手指向內滑動pop當前的viewcontroller。與此同時,點擊返回鍵能正常的pop當前的viewcontroller。
首先根據WWDC的例子,添加一個新類:
#import <UIKit/UIKit.h> @interface ZJSliderTransition : UIPercentDrivenInteractiveTransition - (instancetype)initWithNavigationController:(UINavigationController *)nc; @property(nonatomic,assign) UINavigationController *parent; @property(nonatomic,assign,getter = isInteractive) BOOL interactive; @end
注意源文件中須要添加一些變量,而且在初始化的時候添加gesture:
#import "ZJSliderTransition.h" @interface ZJSliderTransition () { CGFloat _startScale; } @end @implementation ZJSliderTransition - (instancetype)initWithNavigationController:(UINavigationController *)nc; { if (self = [super init]) { self.parent = nc; UIPinchGestureRecognizer *pintchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; [self.parent.topViewController.view addGestureRecognizer:pintchGesture]; } return self; } - (void)handlePinch:(UIPinchGestureRecognizer *)gr { CGFloat scale = [gr scale]; switch ([gr state]) { case UIGestureRecognizerStateBegan: self.interactive = YES; _startScale = scale;
self.parent.delegate = self.parent.topViewController;
[self.parent popViewControllerAnimated:YES];
break; case UIGestureRecognizerStateChanged: { CGFloat percent = (1.0 - scale/_startScale); [self updateInteractiveTransition: (percent <= 0.0) ? 0.0 : percent]; break; } case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: if([gr velocity] >= 0.0 || [gr state] == UIGestureRecognizerStateCancelled) [self cancelInteractiveTransition]; else [self finishInteractiveTransition]; self.interactive = NO; break; default: break; } } @end
因而可知,gesture的狀態和交互式的狀態,是一一對應的。由於咱們但願添加的動畫不影響正常的返回pop,咱們在pinch操做開始的時候,再設置navController的delegate。固然,這樣的設置有點怪。
接下來,就是添加咱們的sliderTransition。爲了和其餘transition區分,咱們給ZJToViewController添加一個BOOL屬性:isPopInterActive。
當isPopInterActive爲YES的時候,咱們纔去準備navController的delegate須要實現的相關對象。
ZJViewController類添加的部分:
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (self.isPopInterActive) { _sliderTransition = [[ZJSliderTransition alloc] initWithNavigationController:self.navigationController]; } } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; if (self.isPopInterActive) { self.navigationController.delegate = nil; } } #pragma mark - UINavigationController - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { if (self.isPopInterActive) { return [[ZJSliderTransitionDelegateObj alloc] init]; } else { return nil; } } - (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController { if (self.isPopInterActive) { return self.sliderTransition; } return nil; }
而後,在masterViewController部分,push一個新的ZJViewController便可。具體的效果請自行編譯運行文後的源碼。
從構建一個交互式的transition能夠看到,交互式自己就被設計爲一個單獨的「模塊」,方便開發的時候集成。這也再次體現出蘋果對開發者的「體貼」。
最後附上本篇的代碼下載地址。
因爲最近轉戰C,iOS的內容拖了又拖,若是有疏漏的地方,歡迎你們指正,謝謝!