在iOS 7
以後,蘋果就開放了自定義轉場的相關api
,如今都快iOS 12
了,一直都沒有好好研究轉場動畫,一個是以前沒有重視,以爲花裏胡哨的,另一個是所作的項目中沒有這樣的轉場動畫需求。這裏說的轉場動畫和上一篇CAAnimation 系統動畫中CATransition
動畫不是一個概念,上一篇指的是單個View的轉場特效,這裏指的是整個控制器的轉場特效。其實寫上篇文章的目前也是爲今天打下鋪墊,複雜的轉場效果也是由單個動畫來組成的。ios
由圖中能夠看出要完成自定義轉場動畫,必須聽從UIViewControllerAnimatedTransitioning
協議,協議中有兩個必須實現的方法一個是返回轉場時間,一個是具體轉場的實現。文章會結合5個最經常使用的動畫場景來講明轉場動畫。git
先來看看網易嚴選App的轉場效果,能夠看出當前頁面想要Push
其餘的頁面的時候,當前頁面會下沉同時其餘頁面從右邊平移至左邊。Present
頁面的時候,當前頁面也會下沉,目標視圖從底部彈出。 github
來看代碼,在ViewController
裏面有兩個按鈕,分別是Push
出SecondVC
和Present
出ThirdVC
。api
- (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];
}
複製代碼
這裏新建一個AnimatedTransitioningObject
類,而後要遵循UIViewControllerAnimatedTransitioning
協議。這個爲了方便,把Push、Pop、Present、Dismiss
這四個效果寫在一塊兒,用枚舉來區分,固然也能夠把每種動畫效果單獨用一個AnimatedTransitioningObject
類來實現。bash
#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;
}
}
複製代碼
- (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];
}
}];
}
複製代碼
Push
和Pop
是相對的關係,因此在Pop
動畫中,目標視圖和來源視圖互換身份,實現也是用CGAffineTransformIdentity
來還原Push
動畫便可。ui
- (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;
}
}];
}
複製代碼
完成AnimatedTransitioningObject
類後,再返回ViewController
中,ViewController
要遵循UINavigationBarDelegate
和UIViewControllerTransitioningDelegate
,把SecondVC
的transitioningDelegate
設置爲本身。而後根據不一樣的operation
,來返回不一樣的動畫實現。atom
@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;
}
複製代碼
看看實現效果 spa
- (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];
}
}];
}
複製代碼
- (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];
}
}];
}
複製代碼
回到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];
}
複製代碼
新建一個手勢類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;
}
}
複製代碼
回到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的轉場效果,第一次看的時候,以爲很酷炫,如今瞭解了轉場的核心後,以爲不那麼難了,有時間再把它的效果寫出來吧。