iOS 7新出了一個非常好玩的特性,就是View Controllers之間的切換動畫,開發人員也可以在程式中自行定製切換動畫,例如翻轉視圖、縮放視圖、旋轉視圖等等。另一方面,對於iPhone的使用來說,通過pan手勢pop回到之前的視圖,使用起來無疑是非常方便的。最開始使用這個特性時感覺非常驚喜。
本文就來說說如何定製我們自己的動畫切換方案。
首先定義一個實現UINavigationControllerDelegate協議的類NavigationPerfomer。
故事板非常簡單,就是一個UINavigationController和兩個View Controller。但是要在UINavigationController中加入一個Object(這個貌似是Xcode 5的新特性吧,以前比較少用故事板,所以不是很確定),該對象的類為NavigaitonPerfomer類:
隨後要為NavigationPerformer添加一個outlet:當前的UINavigatinController。並將UINavigationController的委託設為NavigationPerformer類。
故事板中其他的View Controller一切照舊,這裡做得非常好,在提供了新的動畫介面之後,在UINavigationController類中實現這些介面並不會影響到其他的View Controller的內容。
接著定義兩個動畫類:PushAnimation和PopAnimation,分別對應Push和Pop動畫。
代碼如下:
PushAnimtaion類:
#import @interface PushAnimation : NSObject @end
#pragma mark - UIViewControllerAnimatedTransitioning Delegate/* 動畫持續的時間 */- (NSTimeInterval)transitionDuration:(id)transitionContext { return 1;}/* 動畫的內容 */- (void)animateTransition:(id)transitionContext { UIViewController *desController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *srcController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; [[transitionContext containerView] addSubview:desController.view]; desController.view.alpha = 0.0; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ srcController.view.transform = CGAffineTransformMakeTranslation(-srcController.view.bounds.size.width, 0.0); desController.view.alpha = 1.0; } completion:^(BOOL finished) { srcController.view.transform = CGAffineTransformIdentity; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }];}
動畫類必須實現UIViewControllerAnimatedTransitioning協議。
協議中的兩個必選方法:
// 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 )transitionContext;// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.- (void)animateTransition:(id )transitionContext;
第一個方法指定動畫持續的時間,以秒為單位。
第二個方法定製動畫的內容。通過transitionContext這個上下文環境可以擷取源視圖控制器,目標視圖控制器:
UIViewController *desController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *srcController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
在進行動畫前,先要將desController的視圖添加到當前[內容] 檢視中。
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); // delay = 0.0, options = 0
該方法才是真正定義動畫內容的關鍵,duration參數指定動畫持續的時間,animations代碼塊給出要執行的變換,當動畫完成後調用completion代碼塊通知transition context,在這裡可以進行如設定視圖的alpha、還原仿射變換等動作。
這裡Push Animation的動畫配置是將srcViewController自左向右划出視窗。
另外一個是PopAnimation類:
@interface PopAnimation : NSObject @end
#pragma mark - UIViewControllerAnimatedTransitioning Delegate- (NSTimeInterval)transitionDuration:(id)transitionContext { return 1;}- (void)animateTransition:(id)transitionContext { UIViewController *desController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *srcController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; [[transitionContext containerView] addSubview:desController.view]; desController.view.alpha = 0.0; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ srcController.view.transform = CGAffineTransformMakeScale(-1.0, 1.0); desController.view.alpha = 1.0; } completion:^(BOOL finished) { srcController.view.transform = CGAffineTransformIdentity; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }];}@end
這裡pop的動畫配置是翻轉srcController的視圖。
接著是關鍵的NavigationPerformer類:
#import @class PushAnimation;@class PopAnimation;@interface NavigationPerfomer : NSObject @property (weak, nonatomic) IBOutlet UINavigationController *navigationController;@property (strong, nonatomic) PushAnimation *pushAnimation;@property (strong, nonatomic) PopAnimation *popAnimation;@property (strong, nonatomic) UIPercentDrivenInteractiveTransition *interactionController;@end
注意,interactionController類非常有用,只有通過該類才能使視圖響應使用者的手勢輸入。
首先看看UINavigationControllerDelegate的方法:
#pragma mark - UINavigationController Delegate- (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{ if (operation == UINavigationControllerOperationPush) { return self.pushAnimation; } else if (operation == UINavigationControllerOperationPop) { return self.popAnimation; } return nil;}- (id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id)animationController { return self.interactionController;}
第一個方法指定不同的動作下的動畫配置,在返回對象時,這些對象必須實現UIViewControllerAnimatedTransitioning協議。
第二個方法指定提供互動性的控制器對象,如果返回nil,那麼視圖將無法響應使用者的手勢,如滑動等。
下面為導航控制器的視圖添加一個Pan手勢:
- (void)awakeFromNib { // 在導航控制器的視圖上添加pan手勢 UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self.navigationController.view addGestureRecognizer:panGesture]; // 初始化動畫配置 self.pushAnimation = [[PushAnimation alloc] init]; self.popAnimation = [[PopAnimation alloc] init];}- (void)pan:(UIPanGestureRecognizer *)pan { UIView *view = self.navigationController.view; if (pan.state == UIGestureRecognizerStateBegan) { // 只有從左向右滑動的手勢才作出響應:pop CGPoint location = [pan locationInView:view]; if (location.x < CGRectGetMidX(view.bounds) && self.navigationController.viewControllers.count > 1) { self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init]; [self.navigationController popViewControllerAnimated:YES]; } } else if (pan.state == UIGestureRecognizerStateChanged) { // 以當前的位移作為進度執行動畫 CGPoint translation = [pan translationInView:view]; CGFloat progress = fabs(translation.x / CGRectGetWidth(view.bounds)); [self.interactionController updateInteractiveTransition:progress]; } else if (pan.state == UIGestureRecognizerStateEnded) { if ([pan velocityInView:view].x > 0) { [self.interactionController finishInteractiveTransition]; } else { // 如果手勢的終點相對於起點在x正向上的位移小於0,即為取消手勢 [self.interactionController cancelInteractiveTransition]; } self.interactionController = nil; // 最後必須將interactionController清空,確保不會影響到後面的動畫執行 }}
在pan:方法的執行過程中,程式會根據手勢位移的百分比作為進度播放動畫。
在切換動畫完畢後,一定要將interactionController設為nil,因為某些動畫是不需要手勢互動的,這樣會帶來奇怪的行為。
最後是一些運行結果:
Push時的效果:
使用Pan手勢Pop時的效果:
下載看看。
這部分還是挺好玩的,Just enjoy.
參考資料:
iOS 7 新特性:視圖控制器切換API
objc.io #5 View Controller Transitions
objcio / issue5-view-controller-transitions