iOS7.0后蘋果提供了自定義轉場動畫的API,利用這些API我們可以改變 push和pop(navigation非模態),present和dismiss(模態),標簽切換(tabbar)的默認轉場動畫。
主要涉及的API
1、UIViewControllerAnimatedTransitioning:轉場動畫協議,實現此協議定義轉場的動畫行為。
// 定義轉場動畫的時間
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// 定義轉場動畫的行為
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
2、 UIViewControllerContextTransitioning:轉場動畫上下文,這個協議定義了轉場動畫具體參數,控制轉場動畫的狀態,這個協議一般由系統實現,在轉場發生時提供給我們使用。
From和To:轉場是兩個視圖控制器(ViewController)的行為,由一個視圖控制器切換到另一個視圖控制器,原先呈現的視圖控制器叫FromViewController,將要呈現的視圖控制器叫ToViewController,那么FromViewController的view叫做FromView,ToViewController的view叫做ToView。
對應push和pop來說是兩個不同的轉場,它們的From和To在兩個轉場中使相互調換的。
containerView:轉場動畫完成都是在containerView里面。
3、UIViewControllerInteractiveTransitioning:轉場的交互協議,用來控制轉場動畫的狀態或進度。
//設置轉場進度, 取值范圍 [0..1]
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//完成轉場,呈現to
- (void)finishInteractiveTransition;
//取消轉場,呈現from
- (void)cancelInteractiveTransition;
4、UIPercentDrivenInteractiveTransition:官方提供的實現UIViewControllerInteractiveTransitioning協議的類,可以直接使用。
上面簡單的介紹了轉場動畫涉及的API,這一節主要通過導航控制器的push和pop轉場動畫來介紹這些自定義轉場動畫的流程。
push轉場動畫
1、準備工作:
帶有導航控制器的ViewController類,要push到的下一級控制器SecondViewController類。
2、在類HSPushAnimation中實現UIViewControllerAnimatedTransitioning協議,定義push轉場動畫行為。
@interface HSPushAnimation : NSObject <UIViewControllerAnimatedTransitioning>
@end
@implementation HSPushAnimation
//設置轉場動畫的時長
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 2.f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
//from
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//to
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView* toView = nil;
UIView* fromView = nil;
//UITransitionContextFromViewKey和UITransitionContextToViewKey定義在iOS8.0以后的SDK中,所以在iOS8.0以下SDK中將toViewController和fromViewController的view設置給toView和fromView
//iOS8.0 之前和之后view的層次結構發生變化,所以iOS8.0以后UITransitionContextFromViewKey獲得view并不是fromViewController的view
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
toView = [transitionContext viewForKey:UITransitionContextToViewKey];
} else {
fromView = fromViewController.view;
toView = toViewController.view;
}
//這個非常重要,將toView加入到containerView中
[[transitionContext containerView] addSubview:toView];
CGFloat width = [UIScreen mainScreen].bounds.size.width;
CGFloat height = [UIScreen mainScreen].bounds.size.height;
toView.frame = CGRectMake(width, 0, width, height);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toView.frame = CGRectMake(0, 0, width, height);
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
@end
上面代碼定義了一個非常簡單的動畫,toView從左到右覆蓋fromView,和系統默認動畫一樣,只是時間設置的比較長。
轉場動畫所有要呈現的元素都要放在containerView中,fromView默認已經在containerView中了。
3、指定push要使用的轉場動畫行為:由于要自定義轉場動畫所以我們需要指定轉場動畫行為。push轉場的動畫行為是由UINavigationControllerDelegate協議指定,所以我們在ViewController設置導航控制器的Delegate:
- (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 [[HSPushAnimation alloc] init];
}
return nil;
}
這個方法可以分別指定push和pop的動畫類,這里我們只定義push動畫,所以只要指定UINavigationControllerOperationPush時的動畫行為即可。
這樣push轉場動畫就完成了,效果圖如下:
pop轉場動畫
1、pop轉場動畫和push轉場動畫類似,在類HSPopAnimation中實現UIViewControllerAnimatedTransitioning協議。
@implementation HSPopAnimation
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.5;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView* toView = nil;
UIView* fromView = nil;
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
toView = [transitionContext viewForKey:UITransitionContextToViewKey];
} else {
fromView = fromViewController.view;
toView = toViewController.view;
}
//將toView加到fromView的下面,非常重要!!!
[[transitionContext containerView] insertSubview:toView belowSubview:fromView];
CGFloat width = [UIScreen mainScreen].bounds.size.width;
CGFloat height = [UIScreen mainScreen].bounds.size.height;
fromView.frame = CGRectMake(0, 0, width, height);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromView.frame = CGRectMake(width, 0, width, height);
} completion:^(BOOL finished) {
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
@end
這里pop動畫基本和push動畫是相反的過程,當然你也可以指定別的方式的動畫。
這里from、to和push動畫里面的from、to值已經互換了,所以如果將push和pop動畫寫在一起的話,要特別注意,不過建議將push和pop動畫分別定義到不同的類中,方便管理。
2、在SecondViewControlle類中設置導航控制器的Delegate,并實現以下協議:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPop) {
return [[HSPopAnimation alloc] init];
}
return nil;
}
pop轉場動畫就完成了,效果圖如下:
可交互轉場動畫
先來看看可交互轉場動畫效果
可交互轉場動畫的實現需要實現UIViewControllerInteractiveTransitioning協議,幸好官方給我們提供了UIPercentDrivenInteractiveTransition類可以直接使用,你也可以繼承UIPercentDrivenInteractiveTransition來使用。
UIViewControllerInteractiveTransitioning協議的功能主要是控制轉場動畫的狀態,即動畫完成的百分比,所以只有在轉場中才有用。
比如我們通過[self.navigationController popViewControllerAnimated:YES]
觸發pop轉場動畫,然后在轉場動畫結束之前通過- (void)updateInteractiveTransition:(CGFloat)percentComplete
更改轉場動畫的完成的百分比,那么轉場動畫將由實現UIViewControllerInteractiveTransitioning的類接管,而不是由定時器管理,之后就可以隨意設置動畫狀態了。
交互動畫往往配合手勢操作,手勢操作產生一序列百分比數通過updateInteractiveTransition方法實時更新轉場動畫狀態。
1、現在添加為SecondViewControlle的view添加手勢:
//添加pan手勢
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(panGestureRecognizerAction:)];
[self.view addGestureRecognizer:pan];
2、觸發轉場動畫,通過手勢產生百分比數值,更新轉場動畫狀態:
- (void)panGestureRecognizerAction:(UIPanGestureRecognizer *)pan{
//產生百分比
CGFloat process = [pan translationInView:self.view].x / ([UIScreen mainScreen].bounds.size.width);
process = MIN(1.0,(MAX(0.0, process)));
if (pan.state == UIGestureRecognizerStateBegan) {
self.interactiveTransition = [UIPercentDrivenInteractiveTransition new];
//觸發pop轉場動畫
[self.navigationController popViewControllerAnimated:YES];
}else if (pan.state == UIGestureRecognizerStateChanged){
[self.interactiveTransition updateInteractiveTransition:process];
}else if (pan.state == UIGestureRecognizerStateEnded
|| pan.state == UIGestureRecognizerStateCancelled){
if (process > 0.5) {
[ self.interactiveTransition finishInteractiveTransition];
}else{
[ self.interactiveTransition cancelInteractiveTransition];
}
self.interactiveTransition = nil;
}
}
手勢開始狀態:手勢開始時創建UIPercentDrivenInteractiveTransition對象,通過popViewControllerAnimated方法觸發轉場動畫。
手勢變化狀態:通過計算得到的百分比實時更新轉場動畫的狀態。
手勢取消或者結束狀態:根據完成的百分比決定是否完成轉場或者取消轉場。
3、開始轉場動畫時,就需要指定一個實現UIViewControllerInteractiveTransitioning協議的對象來控制轉場動畫的狀態,否則轉場動畫狀態由定時器管理。在SecondViewControlle類中,我們通過UINavigationControllerDelegate協議將interactiveTransition對象傳給UIKit:
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
self.navigationController.delegate = self;
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{
if ([animationController isKindOfClass:[HSPopAnimation class]]) {
return self.interactiveTransition;
}
return nil;
}
以上步驟就將pop的可交互轉場動畫完成了。
push和pop轉場動畫基本流程
Note:
- 動畫的狀態和轉場的狀態是不一樣的,動畫完成后,不代表轉場完成,所以我們要在動畫的completion里面決定是否完成轉場:[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
- 轉場是一個過程,所有的動畫都在containerView里面完成。
- 不需要交互的轉場interactionControllerForAnimationController方法一定要返回nil
?文章的源碼將在完成完成下篇present和dismiss動畫時候上傳。
本文作為讀書筆記,不是科普讀物,所以知識有可能理解錯誤,如有請您不吝賜教。
Demo地址:https://github.com/cnthinkcode/HSPresentTransitionDemo