iOS 7 以協議的方式開放了自定義轉場的 API,協議的好處是不再拘泥于具體的某個類,只要是遵守該協議的對象都能參與轉場,使其可以非常靈活的使用。轉場協議由5種協議組成,實際中只需要使用其中的兩個或三個便能實現絕大部分的轉場動畫。下面就簡單講解一下使用的心得體會,最后的總結你會發現很簡單。
參考: Custom Transitions Using View Controllers
喵神的博客
1 概念
- 動畫控制器 (Animation Controllers) 遵守 UIViewControllerAnimatedTransitioning 協議,并且負責實際執行動畫。
- 交互控制器 (Interaction Controllers) 通過遵守 UIViewControllerInteractiveTransitioning 協議,來控制可交互式(手勢或重力感應…)動畫轉場,大多都是使用它的一個子類
UIPercentDrivenInteractiveTransition
來更簡單的實現手勢交互動畫。 - 轉場代理 (Transitioning Delegates) 根據不同的轉場類型,提供需要的動畫控制器和交互控制器。
有3種轉場代理:
UINavigationControllerDelegate –自定義navigationController轉場動畫的時候
UITabBarControllerDelegate –自定義tabbarController轉場動畫的時候
UIViewControllerTransitioningDelegate–自定義present/dismiss的時候 - 轉場上下文 (Transitioning Context) 提供轉場中需要的數據,比如在轉場過程中所參與的視圖控制器和視圖的相關屬性。 轉場上下文對象遵守 UIViewControllerContextTransitioning 協議,并且這是由系統負責生成和提供的。
轉場協調器(Transition Coordinators) 可以在運行轉場動畫時,并行的運行其他動畫。轉場協調器遵守UIViewControllerTransitionCoordinator 協議
2 自定義轉場動畫時你可能用到的那些方法
.
UIViewControllerContextTransitioning
這個接口用來提供切換的上下文給開發者使用,包含了從哪個VC到哪個VC等各類信息,一般不需要開發者自己實現。具體來說,iOS7的自定義切換目的之一就是切換相關代碼解耦,在進行VC切換時,做切換效果實現的時候必須需要 切換前后VC的一些信息,提供一些方法,以供我們使用。
-(UIView *)containerView;
>VC切換所發生的view容器,開發者應該將切出的view移除,將切入的view加入到該view容器中。
-(UIViewController *)viewControllerForKey:(NSString *)key;
>提供一個key,返回對應的VC。現在的SDK中key的選擇只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey兩種,分別表示將要切出和切入的VC。
-(CGRect)initialFrameForViewController:(UIViewController *)vc;
>某個VC的初始位置,可以用來做動畫的計算。
-(CGRect)finalFrameForViewController:(UIViewController *)vc;
> 與上面的方法對應,得到切換結束時某個VC應在的frame。
-(void)completeTransition:(BOOL)didComplete;
>向這個context報告切換已經完成。
UIViewControllerAnimatedTransitioning
這個接口負責切換的具體內容,即“切換中應該發生什么”。開發者在做自定義切換效果時大部分代碼會是用來實現這個接口。它只有兩個方法需要我們實現:
-(NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext;
>系統給出一個切換上下文,我們根據上下文環境返回這個切換所需要的花費時間(一般就返回動畫的時間就好了,系統會用這個時間來在百分比驅動的切換中進行幀的計算)。
-(void)animateTransition:(id < UIViewControllerContextTransitioning >)transitionContext;
>在進行切換的時候將調用該方法,我們對于切換時的UIView的設置和動畫都在這個方法中完成。
UIViewControllerTransitioningDelegate
這個接口的作用比較簡單單一,在需要VC切換的時候系統會像實現了這個接口的對象詢問是否需要使用自定義的切換效果。這個接口共有四個類似的方法:
-(id< UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
>在 presented 動畫中會被調用
-(id< UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController *)dismissed;
>在 dismiss 動畫中會被調用
-(id< UIViewControllerInteractiveTransitioning >)interactionControllerForPresentation:(id < UIViewControllerAnimatedTransitioning >)animator;
-(id< UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id < UIViewControllerAnimatedTransitioning >)animator;
UIPercentDrivenInteractiveTransition
這是一個實現了UIViewControllerInteractiveTransitioning接口的類,為我們預先實現和提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。一般來說我們更多地會使用某些手勢來完成交互式的轉移,這樣使用這個類(一般是其子類,下面會講到)的話就會非常方便。我們在手勢識別中只需要告訴這個類的實例當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,十分方便。具體的幾個重要方法:
-(void)updateInteractiveTransition:(CGFloat)percentComplete
> 更新百分比,一般通過手勢識別的長度之類的來計算一個值,然后進行更新。之后的例子里會看到詳細的用法
-(void)cancelInteractiveTransition
>報告交互取消,返回切換前的狀態
–(void)finishInteractiveTransition
>報告交互完成,更新到切換后的狀態
UINavigationControllerDelegate
自定義navigationController轉場動畫的時候
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
>視圖控制器轉換返回一個非交互式動畫對象使用
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
>視圖控制器轉換返回一個互動的動畫對象使用。
3 實戰
- Present 動畫
就如上面所說轉場動畫遵循
UIViewControllerAnimatedTransitioning
協議拿到需要做動畫的視圖,首先建一個動畫類遵循這個協議。
@interface BouncePresentAnimation : NSObject <UIViewControllerAnimatedTransitioning>
遵循這個協議后實現里面的兩個方法
方法1:用來控制轉場動畫的時間
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.8f;
}
方法2:這里用來控制視圖切換做動畫,因為有 present 和 dismiss 所以在這里區分兩個動畫(需要一個type,后面會提到)
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
switch (self.type)
{
case TransitionTypePresent:
[self presentAnimation:transitionContext];
break;
case TransitionTypeDissmiss:
[self dismissAnimation:transitionContext];
break;
default:
break;
}
}
實現:具體也可以看 喵神的博客 此處為借鑒,旨在理解。
//實現present動畫邏輯代碼
\\- (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
// 1. Get controllers from transition context
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
//fromVC.view.hidden = YES;
UIView * screenSnapShotView = [fromVC.view snapshotViewAfterScreenUpdates:YES];
// 2. Set init frame for toVC
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
toVC.view.frame = CGRectOffset(finalFrame, 0, screenBounds.size.height);
// 3. Add toVC's view to containerView
UIView *containerView = [transitionContext containerView];
[containerView insertSubview:screenSnapShotView aboveSubview:fromVC.view];
[containerView addSubview:toVC.view];
// 4. Do animate now
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration
delay:0.0
usingSpringWithDamping:0.6
initialSpringVelocity:0.0
options:UIViewAnimationOptionTransitionFlipFromBottom
animations:^{
toVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
// 5. Tell context that we completed.
[transitionContext completeTransition:YES];
}];
}
//實現dismiss動畫邏輯代碼
- (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
{
// 1. Get controllers from transition context
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 2. Set init frame for fromVC
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect initFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect finalFrame = CGRectOffset(initFrame, 0, screenBounds.size.height);
// 3. Add target view to the container, and move it to back.
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
// 4. Do animate now
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
fromVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
上面我們完成了動畫部分,接下來就是要告知系統,讓其去使用我們的動畫。就如上面提到的需要實現UIViewControllerTransitioningDelegate
,在這里你會知道present和dismiss,所以在這里你可以傳個type
告知你需要的是那種動畫,從而讓你的動畫類去實現相應動畫。
present 時會觸發的代理,這個時候告訴它我們需要的present動畫
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return [BouncePresentAnimation transitionWithTransitionType:TransitionTypePresent];
}
dismiss 時會觸發的代理,這個時候告訴它我們需要的dismiss動畫
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [BouncePresentAnimation transitionWithTransitionType:TransitionTypeDissmiss];
}
當然前提是你遵從了它的代理,你可以讓前一個控制器作為后者的代理去實現這些方法。
ModalViewController *mvc = [[ModalViewController alloc] init];
mvc.transitioningDelegate = self;
mvc.delegate = self;
[self presentViewController:mvc animated:YES completion:nil];
- pop 動畫
和present一樣,如果想要自定義動畫,需要遵循
UIViewControllerAnimatedTransitioning
協議
@interface PushAnimation : NSObject<UIViewControllerAnimatedTransitioning>
同樣的實現協議里的兩個方法
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
return 1.0f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
self.transitionContext = transitionContext;
switch (self.type)
{
case TransitionAnimTypePop:
[self transitionAnimTypePopWithTransitionContext:transitionContext];
break;
case TransitionAnimTypePush:
[self transitionAnimTypePushWithTransitionContext:transitionContext];
break;
default:
break;
}
}
同樣的,你需要遵守 UINavigationControllerDelegate
,并在它的代理方法里面實現你要的動畫
添加代理
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationController.delegate = self;
}
實現動畫
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush)
{
return [PushAnimation transitionWithTransitionType:
TransitionAnimTypePush];
}
else if (operation == UINavigationControllerOperationPop)
{
return [PushAnimation transitionWithTransitionType:
TransitionAnimTypePop];
}
//返回nil則使用默認的動畫效果
return nil;
}
- 實現手勢動畫
如上所述,手勢動畫需要
UIPercentDrivenInteractiveTransition
實現,提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。我們在手勢識別中只需要告訴這個類的實例當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,使動畫過渡的更加自然。
首先我們新建一個手勢處理類SwipeUpInteractiveTransition
繼承于UIPercentDrivenInteractiveTransition
這樣我們可以獲取相應的父類方法。
首先給所在的控制器的view添加手勢
[self.transitionController wireToViewController:mvc];
主要運用的手勢控制
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
// 1. Mark the interacting flag. Used when supplying it in delegate.
self.interacting = YES;
[self.presentingVC dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged:
{
// 2. Calculate the percentage of guesture
CGFloat fraction = (translation.y / KWindowHeight);
//Limit it between 0 and 1
fraction = fminf(fmaxf(fraction, 0.0), 1.0);
NSLog(@"fraction==%f",fraction);
self.shouldComplete = (fraction > 0.5);
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
{
// 3. Gesture over. Check if the transition should happen or not
self.interacting = NO;
if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled)
{
[self cancelInteractiveTransition];
}
else
{
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
- 總結
非交互動畫
1.創建動畫類遵從UIViewControllerAnimatedTransitioning
實現里面的接口
2.讓相應的控制器做代理
3.present 實現UIViewControllerTransitioningDelegate
接口,push 實現UINavigationControllerDelegate
接口
交互動畫
1.創建動畫類繼承于UIPercentDrivenInteractiveTransition
,然后在里面實現相應的動畫即可。
2.在跳轉頁面時告訴動畫類你需要控制的頁面
注:在push動畫中的layer動畫可以任意替換為其他轉場動畫,只用把動畫加到 containerView.layer
上即可。CATransition
動畫傳送門
containerView.layer addAnimation:transition forKey:nil`
照例放Demo,僅供參考
Demo地址:
https://github.com/yongliangP/iOS-TransitionVC
如果你覺得對你有幫助請點喜歡哦,也可以關注我,每周至少一篇技術。
或者關注 我的專題 每周至少5篇更新,多謝支持哈。