在iOS 7
之後,蘋果就開放了自定義轉場的相關api
,現在都快iOS 12
了,一直都沒有好好研究轉場動畫,一個是之前沒有重視,覺得花裡胡哨的,另外一個是所做的專案中沒有這樣的轉場動畫需求。這裡說的轉場動畫和上一篇CAAnimation 系統動畫中CATransition
動畫不是一個概念,上一篇指的是單個View的轉場特效,這裡指的是整個控制器的轉場特效。其實寫上篇文章的目前也是為今天打下鋪墊,複雜的轉場效果也是由單個動畫來組成的。
由圖中可以看出要完成自定義轉場動畫,必須遵從UIViewControllerAnimatedTransitioning
協議,協議中有兩個必須實現的方法一個是返回轉場時間,一個是具體轉場的實現。文章會結合5個最常用的動畫場景來說明轉場動畫。
先來看看網易嚴選App的轉場效果,可以看出當前頁面想要Push
其他的頁面的時候,當前頁面會下沉同時其他頁面從右邊平移至左邊。Present
頁面的時候,當前頁面也會下沉,目標檢視從底部彈出。
來看程式碼,在ViewController
裡面有兩個按鈕,分別是Push
出SecondVC
和Present
出ThirdVC
。
- (IBAction)pushBtnClick:(id)sender
{
SecondViewController * vc = [[SecondViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
- (IBAction)presentBtnClick:(id)sender
{
ThirdViewController * vc = [[ThirdViewController alloc] init];
[self presentViewController:vc animated:YES completion:nil];
}
複製程式碼
Push和Pop動畫
UIViewControllerAnimatedTransitioning協議
這裡新建一個AnimatedTransitioningObject
類,然後要遵循UIViewControllerAnimatedTransitioning
協議。這個為了方便,把Push、Pop、Present、Dismiss
這四個效果寫在一起,用列舉來區分,當然也可以把每種動畫效果單獨用一個AnimatedTransitioningObject
類來實現。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger,TransitionAnimationObjectType) {
TransitionAnimationObjectType_Push,
TransitionAnimationObjectType_Pop,
TransitionAnimationObjectType_present,
TransitionAnimationObjectType_Dismiss
};
@interface TransitionAnimationObject : NSObject <UIViewControllerAnimatedTransitioning>
@property (nonatomic,assign) TransitionAnimationObjectType type;
- (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;
+ (instancetype)initWithTransitionAnimationObjectType:(TransitionAnimationObjectType)type;
@end
複製程式碼
來看看兩個必須實現的方法,在返回轉場時間裡也可以根據type
來返回不同的動畫時間,這裡統一返回0.5秒。pushAnimateTransition
裡面實現Push
效果轉場。
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
switch (_type) {
case TransitionAnimationObjectType_Push:
[self pushAnimateTransition:transitionContext];
break;
case TransitionAnimationObjectType_Pop:
[self popAnimateTransition:transitionContext];
break;
case TransitionAnimationObjectType_present:
[self presentAnimateTransition:transitionContext];
break;
case TransitionAnimationObjectType_Dismiss:
[self dismissAnimateTransition:transitionContext];
break;
default:
break;
}
}
複製程式碼
Push實現
- (void)pushAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//獲取目標View(secondVC.view) 和 來源View(ViewController.view)
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//這裡截圖做動畫 隱藏來源View
UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
fromView.hidden = YES;
//將需要做轉場的View按照順序新增到轉場容器中
UIView * containerView = [transitionContext containerView];
[containerView addSubview:tempView];
[containerView addSubview:toView];
CGFloat width = containerView.frame.size.width;
CGFloat height = containerView.frame.size.height;
//設定目標View的初始位置
toView.frame = CGRectMake(width, 0, width, height);
//開始做動畫
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
toView.transform = CGAffineTransformMakeTranslation(-width, 0);
} completion:^(BOOL finished) {
//這裡要標記轉場成功 假如不標記 系統會認為還在轉場中 無法互動
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
//轉場失敗 也要做相應的處理
if ([transitionContext transitionWasCancelled])
{
fromView.hidden = NO;
[tempView removeFromSuperview];
}
}];
}
複製程式碼
Pop實現
Push
和Pop
是相對的關係,所以在Pop
動畫中,目標檢視和來源檢視互換身份,實現也是用CGAffineTransformIdentity
來還原Push
動畫即可。
- (void)popAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//注意這裡是還原 所以toView和fromView 身份互換了 toView是ViewController.view
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//獲取相應的檢視
UIView * containerView = [transitionContext containerView];
UIView * tempView = [[containerView subviews] firstObject];
//在fromView 下面插入toView 不然回來的時候回黑屏
[containerView insertSubview:toView belowSubview:fromView];
//將動畫直接還原即可
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
tempView.transform = CGAffineTransformIdentity;
fromView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
//標記轉場
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
//轉場成功的處理
if (![transitionContext transitionWasCancelled])
{
[tempView removeFromSuperview];
toView.hidden = NO;
}
}];
}
複製程式碼
UINavigationControllerDelegate代理方法
完成AnimatedTransitioningObject
類後,再返回ViewController
中,ViewController
要遵循UINavigationBarDelegate
和UIViewControllerTransitioningDelegate
,把SecondVC
的transitioningDelegate
設定為自己。然後根據不同的operation
,來返回不同的動畫實現。
@interface ViewController () <UINavigationControllerDelegate,UIViewControllerTransitioningDelegate>
- (IBAction)pushBtnClick:(id)sender
{
SecondViewController * vc = [[SecondViewController alloc] init];
vc.transitioningDelegate = self;
[self.navigationController pushViewController:vc animated:YES];
}
#pragma mark - Push && Pop
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush)
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Push];
}
else if (operation == UINavigationControllerOperationPop)
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Pop];
}
return nil;
}
複製程式碼
看看實現效果
Present動畫和Dismiss動畫
Present實現
- (void)presentAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//獲取目標View(ThirdVC.view) 和 來源View(ViewController.view)
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//截圖做動畫
UIView * tempView = [fromView snapshotViewAfterScreenUpdates:NO];
tempView.frame = fromView.frame;
fromView.hidden = YES;
//按照順序假如轉場動畫容器中
UIView * containerView = [transitionContext containerView];
[containerView addSubview:tempView];
[containerView addSubview:toView];
CGFloat width = containerView.frame.size.width;
CGFloat height = containerView.frame.size.height;
//設定toView的初始化位置 在螢幕底部
toView.frame = CGRectMake(0, height, width, 400);
//做轉場動畫
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
tempView.transform = CGAffineTransformMakeScale(0.9, 0.9);
toView.transform = CGAffineTransformMakeTranslation(0, -400);
} completion:^(BOOL finished) {
//轉場結束後一定要標記 否則會認為還在轉場 無法互動
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
if ([transitionContext transitionWasCancelled])
{
//轉場失敗
fromView.hidden = NO;
[tempView removeFromSuperview];
}
}];
}
複製程式碼
Dismiss實現
- (void)dismissAnimateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//dismiss的時候 fromVC和toVC身份倒過來了
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
//containerView裡面的順序也倒過來了 截圖在最上面
UIView * containerView = [transitionContext containerView];
UIView * tempView = [[containerView subviews] firstObject];
//做還原動畫就可以了
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.55 initialSpringVelocity:1 options:0 animations:^{
tempView.transform = CGAffineTransformIdentity;
fromView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
//轉場結束後一定要標記 否則會認為還在轉場 無法互動
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
if (![transitionContext transitionWasCancelled])
{
//轉場成功
toView.hidden = NO;
[tempView removeFromSuperview];
}
}];
}
複製程式碼
UIViewControllerTransitioningDelegate代理方法
回到ViewController
,把ThirdVC
的transitioningDelegate
設定為自己,然後在代理方法中自定型別。
- (IBAction)presentBtnClick:(id)sender
{
ThirdViewController * vc = [[ThirdViewController alloc] init];
vc.transitioningDelegate = self;
[self presentViewController:vc animated:YES completion:nil];
}
#pragma mark - Present && Dismiss
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_present];
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [TransitionAnimationObject initWithTransitionAnimationObjectType:TransitionAnimationObjectType_Dismiss];
}
複製程式碼
手勢動畫
UIPercentDrivenInteractiveTransition建立手勢類
新建一個手勢類GestureObject
繼承自UIPercentDrivenInteractiveTransition
,addGestureToViewController
是給目標控制器新增手勢。
#import <UIKit/UIKit.h>
@interface GestureObject : UIPercentDrivenInteractiveTransition
//判斷是互動的手勢
@property (nonatomic,assign) BOOL interacting;
- (void)addGestureToViewController:(UIViewController *)viewController;
@end
複製程式碼
然後再手勢的狀態之間來判斷是否執行動畫,這裡是判斷手勢偏移量超過螢幕一半的高度就生效,執行相關動畫,否則還原動畫。
- (void)handleGesture:(UIPanGestureRecognizer *)ges
{
CGPoint point = [ges translationInView:ges.view];
switch (ges.state) {
case UIGestureRecognizerStateBegan:
{
self.interacting = YES;
[self.targetVC dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged:
{
CGFloat fraction = point.y / ges.view.frame.size.height;
//限制在0和1之間
fraction = MAX(0.0, MIN(fraction, 1.0));
self.shouldComplete = fraction > 0.5;
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
{
self.interacting = NO;
if (!self.shouldComplete || ges.state == UIGestureRecognizerStateCancelled)
{
//還原動畫
[self cancelInteractiveTransition];
}
else
{
//完成動畫
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
複製程式碼
UIViewControllerTransitioningDelegate代理方法
回到ViewController
中,在Present
出ThirdVC
的時候新增手勢,在代理方法interactionControllerForDismissal
中指定手勢。
- (IBAction)presentBtnClick:(id)sender
{
ThirdViewController * vc = [[ThirdViewController alloc] init];
vc.transitioningDelegate = self;
[self.gestureObject addGestureToViewController:vc];
[self presentViewController:vc animated:YES completion:nil];
}
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
return self.gestureObject.interacting ? self.gestureObject : nil;
}
複製程式碼
看看效果
小結
Push
、Pop
、Present
、Dismiss
、手勢動畫都講解完了,可以看出,自定義轉場大致的步驟是
- 根據
viewForKey
來獲取轉場上下文 - 將要轉場的檢視加入轉場容器中
- 做出轉場動畫
- 標記轉場成功的狀態,根據狀態做相應的處理
理解了這些,再複雜的轉場動畫都能一步步分解出來,下面是格瓦拉App的轉場效果,第一次看的時候,覺得很酷炫,現在瞭解了轉場的核心後,覺得不那麼難了,有時間再把它的效果寫出來吧。