在iOS中Navigation Controller推入和推出VC,彈出Modal VC或者是Tab Controller切換選中tab時都會有默認的動畫。統一固然是好的,但有時候我們還是想要有點新意,然而要去改變這些默認的效果還是比較麻煩。但是自從有了轉場動畫之後,生活瞬間就變得美好起來了。
(先吐槽下這個名字,拗口)
轉場動畫在iOS7中引入,可以分為兩類:非交互式和交互式。顧名思義,二者的區別就在於用戶是不是可以干預動畫的執行進度。接下來,讓我們先暫時忘掉這兩個概念。
總的來說,轉場動畫由兩個部件組成:Animation Controller和Animation Delegate。它們通過以下方式結合在一起:
系統:hey, Delegate,我要做頁面切換了,需要自定義動畫嗎?
Delegate:要啊,來,我給你個動畫控制器,你就照著這個做吧。
系統:......,好吧(原本以為我是大爺的)。
系統默默地去執行動畫控制器定制的動畫了。
動畫控制器
實現動畫控制器時需要實現UIViewControllerAnimatedTransitioning協議,一般我們需要實現的方法為:
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;
:告訴系統自定義動畫持續的時間,系統通過這個時間來保證頁面切換動畫和頁面切換動畫發生時同時發生的動畫保持一致。比如,push頁面的時候,導航欄的變化。
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
:提供自定義的動畫實現。
在上面的接口中都出現了transitionContext這個參數,它由系統負責初始化,用於描述轉場動畫的上下文信息。它實現了UIViewControllerContextTransitioning協議,通過它我們可以得到如from/toView[Controller], containerView等信息。
UINavigationControllerDelegate
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
operation會告訴我們是Push還是Pop
UIViewControllerTransitioningDelegate
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
為頁面呈現和消失分別定義了一個方法。
了解上述內容後,要使用轉場動畫就很簡單了。首先我們需要定義一個動畫控制器,然後我們還需要定義一個Delegate來告訴系統使用我們的控制器。再然後就沒有然後了,一切就交給系統去完成吧。下面來看個具體的例子。
定義動畫控制器
我為Push和Pop分別定義了動畫控制器,大致實現接近,這裡只看下Push的實現:
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return TRANSITION_DURATION;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toView];
toView.frame = containerView.bounds;
CATransform3D scaleTransform = CATransform3DMakeScale(0.6f, 0.6f, 0.6f);
CATransform3D translationR = CATransform3DMakeTranslation(containerView.frame.size.width, 0, 0);
CATransform3D translationL = CATransform3DMakeTranslation(-containerView.frame.size.width, 0, 0);
toView.layer.transform = translationR;
[UIView animateWithDuration:TRANSITION_DURATION
animations:^{
toView.layer.transform = CATransform3DIdentity;
fromView.layer.transform = CATransform3DConcat(translationL, scaleTransform);
}
completion:^(BOOL finished) {
fromView.layer.transform = CATransform3DIdentity;
BOOL cancelled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!cancelled];
}];
}
定義Delegate
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
if(operation == UINavigationControllerOperationPush){
return self.pushAnimationController;
}
else if(operation == UINavigationControllerOperationPop){
return self.popAnimationController;
}
return nil;
}
之後將NavigationController的delegate設成我們的Delegate就大功告成了,so easy!
有了前面的基礎,我們就已經可以實現很多炫酷的效果了。不過在前面的示例中在用戶按下按鈕後,動畫就開始執行,用戶不能干預動畫的執行過程。按照一開始討論的分類來看,它應該屬於非交互式的。如果我們需要實現類似系統按住屏幕左邊緣可以讓頁面跟隨手指移動的效果,就需要交互式的轉場動畫了。
不過別擔心,別被這些概念唬住,交互式動畫其實就是修飾我們之前定義的動畫。比如系統有個UIPercentDrivenInteractiveTransition交互式動畫實現,它的作用就是根據用戶的行為控制動畫的進度。
要告訴系統要使用交互式動畫,在之前的兩個Delegate上都有對應的方法:
//navigation delegate
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
//view controller delegate
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
接下來,我們就來使用UIPercentDrivenInteractiveTransition跟隨手指的效果。
Delegate
只需要簡單地返回一個交互式動畫控制器,如下:
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{
return self.isInteractiveMode ? self.percentTransition : nil;
}
處理手勢操作
- (void)handlePanGR:(UIScreenEdgePanGestureRecognizer *)panGR{
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
{
//拖動開始,進入交互模式
[self.naviDelegate enterInteractiveMode];
[self.navigationController popViewControllerAnimated:YES];
}
break;
case UIGestureRecognizerStateChanged:
{
//根據拖動距離更新交互式動畫的進度
CGPoint pt = [panGR translationInView:panGR.view];
[self.naviDelegate.percentTransition updateInteractiveTransition:pt.x / panGR.view.frame.size.width];
}
break;
case UIGestureRecognizerStateFailed:
case UIGestureRecognizerStateCancelled:
{
//拖動手勢取消或者失敗,離開交互模式,並通知交互式動畫控制器取消動畫
[self.naviDelegate leaveInteractiveMode];
[self.naviDelegate.percentTransition cancelInteractiveTransition];
}
break;
case UIGestureRecognizerStateEnded:
{
//拖動結束,離開交互模式,並根據移動距離是否要完成動畫
[self.naviDelegate leaveInteractiveMode];
CGPoint pt = [panGR translationInView:panGR.view];
CGFloat delta = 0.0000001;
if(pt.x - panGR.view.frame.size.width / 2 <= delta){
[self.naviDelegate.percentTransition cancelInteractiveTransition];
}
else{
[self.naviDelegate.percentTransition finishInteractiveTransition];
}
}
break;
default:
break;
}
}
轉場動畫的基本內容就介紹完了,除了如何實現動畫本身,我覺得還應該借鑒蘋果的設計方式,職責分離,各自負責各自的事情,減少依賴,便於擴展。
歡迎大家吐槽。
完整Demo