轉(zhuǎn)場動(dòng)畫,就是Vc切換過程中的過渡動(dòng)畫。
官方支持以下幾種方式的自定義轉(zhuǎn)場:
1、我們最常見的在 UINavigationController 中 push 和 pop;
2、也是比較常見的在 UITabBarController 中切換 Tab;
3、Modal 轉(zhuǎn)場:presentation 和 dismissal;
4、UICollectionViewController 的布局轉(zhuǎn)場:UICollectionViewController 與 UINavigationController 結(jié)合的轉(zhuǎn)場方式;
如果需要更定制化的動(dòng)畫就需要自定義設(shè)計(jì)轉(zhuǎn)場了。本文集中于presentation-dismissal的自定義轉(zhuǎn)場動(dòng)畫。
首先這里介紹一下系統(tǒng)自帶的動(dòng)畫效果
@property(nonatomic,assign) UIModalTransitionStyle modalTransitionStyle NS_AVAILABLE_IOS(3_0);
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle NS_AVAILABLE_IOS(3_2);
UIViewController有modalTransitionStyle和modalPresentationStyle這兩個(gè)屬性,
- modalTransitionStyle 系統(tǒng)自帶的幾種過渡動(dòng)畫。
typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
UIModalTransitionStyleCoverVertical = 0, //自下而上覆蓋
UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED, //翻轉(zhuǎn)
UIModalTransitionStyleCrossDissolve, //漸顯
UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, //翻頁效果
};
- modalPresentationStyle 當(dāng)你用present的方式呈現(xiàn)一個(gè)viewController的時(shí)候,可以設(shè)置將要彈出的viewcontroller的展示樣式。
typedefNS_ENUM(NSInteger, UIModalPresentationStyle) {
UIModalPresentationFullScreen =0, //全屏覆蓋
UIModalPresentationPageSheet,//在portrait時(shí)是FullScreen,在landscape時(shí)和FormSheet模式一樣。
UIModalPresentationFormSheet,// 會(huì)將窗口縮小,使之居于屏幕中間。在portrait和landscape下都一樣,但要注意landscape下如果軟鍵盤出現(xiàn),窗口位置會(huì)調(diào)整。
UIModalPresentationCurrentContext,//這種模式下,presented VC的彈出方式和presenting VC的父VC的方式相同。
UIModalPresentationCustom,//自定義視圖展示風(fēng)格,由一個(gè)自定義演示控制器和一個(gè)或多個(gè)自定義動(dòng)畫對(duì)象組成。符合UIViewControllerTransitioningDelegate協(xié)議。使用視圖控制器的transitioningDelegate設(shè)定您的自定義轉(zhuǎn)換。presentingVc的視圖不會(huì)被移除
UIModalPresentationOverFullScreen,//presentingVc的視圖不會(huì)被移除,如果視圖沒有被填滿,presentingVc的視圖可以透過
UIModalPresentationOverCurrentContext,//視圖全部被透過
UIModalPresentationPopover,
UIModalPresentationNone ,
};
關(guān)于系統(tǒng)提供的轉(zhuǎn)場動(dòng)畫可點(diǎn)擊傳送至 http://www.itdecent.cn/p/2a1f3c424cfe
-
presentingViewController和presentedViewController,fromView和toView
The from and to objects
Presented和Presenting是一組相對(duì)的概念,它不受present或dismiss的影響,如果是從A視圖控制器present到B,那么A總是B的presentingViewController,B總是A的presentedViewController。
這張圖示說明,我們還可以理解一下fromView和toView這個(gè)兩個(gè)概念:
fromView表示當(dāng)前視圖toView表示要跳轉(zhuǎn)到的視圖。如果是從A視圖控制器present到B,則A是fromView,B是toView。從B視圖控制器dismiss到A時(shí),B變成了fromView,A是toView。
自定義轉(zhuǎn)場動(dòng)畫中幾個(gè)關(guān)鍵協(xié)議
- 轉(zhuǎn)場協(xié)議 UIViewControllerTransitioningDelegate
UIViewController的transitioningDelegate屬性為遵守UIViewControllerTransitioningDelegate協(xié)議的對(duì)象
// Present過程中返回一個(gè)遵守 <UIViewControllerAnimatedTransitioning> 協(xié)議的對(duì)象,也就是實(shí)現(xiàn)動(dòng)畫過程的對(duì)象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
// dismiss過程中返回一個(gè)遵守 <UIViewControllerAnimatedTransitioning> 協(xié)議的對(duì)象,也就是實(shí)現(xiàn)動(dòng)畫過程的對(duì)象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
//如果delegate實(shí)現(xiàn)了此方法,在轉(zhuǎn)場過程中會(huì)調(diào)用,可根據(jù)是否有手勢來判斷是否返回交互控制對(duì)象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
//如果delegate實(shí)現(xiàn)了此方法,在轉(zhuǎn)場過程中會(huì)調(diào)用,可根據(jù)是否有手勢來判斷是否返回交互控制對(duì)象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
/* 返回一個(gè)UIPresentationController的子類對(duì)象,UIPresentationController,提供了四個(gè)函數(shù)來定義present和dismiss動(dòng)畫開始前后的操作:
1、presentationTransitionWillBegin: present將要執(zhí)行時(shí)
2、presentationTransitionDidEnd: present執(zhí)行結(jié)束后
3、dismissalTransitionWillBegin: dismiss將要執(zhí)行時(shí)
4、dismissalTransitionDidEnd: dismiss執(zhí)行結(jié)束后
*/
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);
- 動(dòng)畫協(xié)議 UIViewControllerAnimatedTransitioning
轉(zhuǎn)場動(dòng)畫的執(zhí)行由此協(xié)議控制
//返回動(dòng)畫執(zhí)行的時(shí)長
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// 轉(zhuǎn)場動(dòng)畫就是在這個(gè)方法里面添加
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
- 轉(zhuǎn)場環(huán)境協(xié)議 UIViewControllerContextTransitioning
動(dòng)畫協(xié)議UIViewControllerAnimatedTransitioning 的兩個(gè)方法都有個(gè)遵守UIViewControllerContextTransitioning協(xié)議的參數(shù),表示的是當(dāng)前轉(zhuǎn)場的上下文,可以獲取到 fromViewController、或者是toViewController以及fromView、toView、contentView等。
簡單示例
Demo地址 https://github.com/YanLYM/YMTransitionDemo
- 漸顯動(dòng)畫
//FromViewController中
- (void)event_present {
YMFadeToViewController *vc = [YMFadeToViewController new];
vc.transitioningDelegate = self;
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:vc animated:YES completion:nil];
}
//實(shí)現(xiàn)代理方法
#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [YMFadeInAnimate new];
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return [YMFadeInAnimate new];
}
//此轉(zhuǎn)場不涉及手勢交互,所以不需要實(shí)現(xiàn)其它協(xié)議方法
//YMFadeInAnimate遵守了UIViewControllerAnimatedTransitioning協(xié)議
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
return 0.5;
}
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
//獲取當(dāng)前上下文的容器
UIView *contentView = [transitionContext containerView];
//fromViewController
UIViewController *fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
//toViewController
UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
fromView.frame = [transitionContext initialFrameForViewController:fromVc];
toView.frame = [transitionContext finalFrameForViewController:toVc];
[contentView addSubview:toView];
NSTimeInterval time = [self transitionDuration:transitionContext];
fromView.alpha = 1;
toView.alpha = 0;
[UIView animateWithDuration:time animations:^{
fromView.alpha = 0;
toView.alpha = 1;
} completion:^(BOOL finished) {
//transitionWasCancelled 這個(gè)方法判斷轉(zhuǎn)場是否已經(jīng)取消了,下面的completeTransition設(shè)置轉(zhuǎn)場完成
//動(dòng)畫結(jié)束后一定要調(diào)用completeTransition方法 來完成或取消轉(zhuǎn)場
BOOL cancelTransition = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!cancelTransition];
}];
}

- 交互控制器協(xié)議 UIViewControllerInteractiveTransitioning
官方提供了一個(gè)已實(shí)現(xiàn)UIViewControllerInteractiveTransitioning協(xié)議的類UIPercentDrivenInteractiveTransition
為我們預(yù)先實(shí)現(xiàn)和提供了一系列便利的方法,可以用一個(gè)百分比來控制交互式切換的過程,利用手勢來完成這個(gè)轉(zhuǎn)場。
//暫停交互
- (void)pauseInteractiveTransition NS_AVAILABLE_IOS(10_0);
//更新方法,一般交互時(shí)候的進(jìn)度更新就在這個(gè)方法里面
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//第三個(gè)是取消交互
- (void)cancelInteractiveTransition;
//第四個(gè)的話就是設(shè)置交互完成
- (void)finishInteractiveTransition;
-
下一個(gè)栗子:手勢切換Vc
手勢切換Vc.gif
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator {
//有手勢 返回處理交互協(xié)議對(duì)象
if (self.gestureRecognizer) {
//YMSwipeInteractiveTransition 繼承自UIPercentDrivenInteractiveTransition
return [[YMSwipeInteractiveTransition alloc] initWithGestureRecognizer:self.gestureRecognizer edgeForDragging:self.targetEdge];
}
return nil;
}
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
if (self.gestureRecognizer) {
return [[YMSwipeInteractiveTransition alloc] initWithGestureRecognizer:self.gestureRecognizer edgeForDragging:self.targetEdge];
}
return nil;
}
//YMSwipeInteractiveTransition 中
-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
[super startInteractiveTransition:transitionContext];
//保存我們的交互上下文,方便做進(jìn)度更新等操作
self.transitionContext = transitionContext;
}
//初始化時(shí)添加手勢action
-(void)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer{
switch (gestureRecognizer.state){
case UIGestureRecognizerStateBegan:
break;
case UIGestureRecognizerStateChanged:
// 調(diào)用updateInteractiveTransition來更新動(dòng)畫進(jìn)度
// 里面嵌套定義 percentForGesture 方法計(jì)算動(dòng)畫進(jìn)度
[self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
break;
case UIGestureRecognizerStateEnded:
//判斷手勢位置,要大于一般,就完成這個(gè)轉(zhuǎn)場,要小于一半就取消
if ([self percentForGesture:gestureRecognizer] >= 0.5f)
// 完成交互轉(zhuǎn)場
[self finishInteractiveTransition];
else
// 取消交互轉(zhuǎn)場
[self cancelInteractiveTransition];
break;
default:
[self cancelInteractiveTransition];
break;
}
}
// 計(jì)算動(dòng)畫進(jìn)度
-(CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture{
UIView * transitionContainerView = self.transitionContext.containerView;
// 手勢滑動(dòng) 在transitionContainerView中 的位置
// 這個(gè)位置判斷的方法可以具體根據(jù)你的需求確定
CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];
CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
CGFloat height = CGRectGetHeight(transitionContainerView.bounds);
if (self.edge == UIRectEdgeRight)
return (width - locationInSourceView.x) / width;
else if (self.edge == UIRectEdgeLeft)
return locationInSourceView.x / width;
else if (self.edge == UIRectEdgeBottom)
return (height - locationInSourceView.y) / height;
else if (self.edge == UIRectEdgeTop)
return locationInSourceView.y / height;
else
return 0.f;
}
這里值得注意的是,UIViewControllerTransitioningDelegate代理中返回 動(dòng)畫協(xié)議對(duì)象 的方法,有些文章說當(dāng)執(zhí)行交互式轉(zhuǎn)場式不會(huì)調(diào)用,經(jīng)測試,在調(diào)用返回交互控制協(xié)議對(duì)象之前會(huì)先調(diào)用 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;因?yàn)閯?dòng)畫還是由其控制,只是進(jìn)度百分比由交互控制協(xié)議對(duì)象控制。
- 轉(zhuǎn)場協(xié)調(diào)器協(xié)議 UIViewControllerTransitionCoordinator
- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation
completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view
animation:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation
completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
- (void)notifyWhenInteractionEndsUsingBlock: (void (^)(id <UIViewControllerTransitionCoordinatorContext>context))handler NS_DEPRECATED_IOS(7_0, 10_0,"Use notifyWhenInteractionChangesUsingBlock");
- (void)notifyWhenInteractionChangesUsingBlock: (void (^)(id <UIViewControllerTransitionCoordinatorContext>context))handler NS_AVAILABLE_IOS(10_0);
可在轉(zhuǎn)場動(dòng)畫發(fā)生的同時(shí)并行執(zhí)行其他的動(dòng)畫,其作用與其說協(xié)調(diào)不如說輔助,主要在 Modal 轉(zhuǎn)場和交互轉(zhuǎn)場取消時(shí)使用,其他時(shí)候很少用到;遵守<UIViewControllerTransitionCoordinator>協(xié)議;由 UIKit 在轉(zhuǎn)場時(shí)生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一個(gè)遵守該協(xié)議的對(duì)象,且該方法只在該控制器處于轉(zhuǎn)場過程中才返回一個(gè)此類對(duì)象,不參與轉(zhuǎn)場時(shí)返回 nil。轉(zhuǎn)場協(xié)調(diào)器的使用可結(jié)合UIPresentationController使用。
-
UIPresentationController
UIPresentationController,它提供了四個(gè)函數(shù)來定義present和dismiss動(dòng)畫開始前后的操作:1、presentationTransitionWillBegin: present將要執(zhí)行時(shí) 2、presentationTransitionDidEnd: present執(zhí)行結(jié)束后 3、dismissalTransitionWillBegin: dismiss將要執(zhí)行時(shí) 4、dismissalTransitionDidEnd: dismiss執(zhí)行結(jié)束后
Example:

如果要實(shí)現(xiàn)圖中點(diǎn)擊空白區(qū)域?qū)崿F(xiàn)dismiss的效果就可以通過UIPresentationController來實(shí)現(xiàn)了,可在presentationTransitionWillBegin: 方法中替換presentedView
/**
present將要執(zhí)行
*/
- (void)presentationTransitionWillBegin {
self.replacePresentView = [[UIView alloc] initWithFrame:[self frameOfPresentedViewInContainerView]];
self.replacePresentView.layer.cornerRadius = 16;
self.replacePresentView.layer.shadowOpacity = 0.44f;
self.replacePresentView.layer.shadowRadius = 13.f;
self.replacePresentView.layer.shadowOffset = CGSizeMake(0, -6.f);
UIView *presentedView = [super presentedView];
presentedView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.replacePresentView addSubview:presentedView];
UIView *dismissView = [[UIView alloc] initWithFrame:self.containerView.bounds];
[self.containerView addSubview:dismissView];
self.dismissView = dismissView;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gesture_dismiss)];
[self.dismissView addGestureRecognizer:tap];
id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
self.dismissView.alpha = 0.f;
self.dismissView.backgroundColor = [UIColor blackColor];
//執(zhí)行轉(zhuǎn)場動(dòng)畫的同時(shí)執(zhí)行其他動(dòng)畫
[transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.dismissView.alpha = 0.5f;
} completion:NULL];
}
- (UIView *)presentedView {
return self.replacePresentView;
}
-
再看一個(gè)擴(kuò)散圓的例子
擴(kuò)散圓.gif
重要的部分就是使用UIBezierPath 和 CABasicAnimation來做動(dòng)畫,懂得了原理,加上會(huì)做動(dòng)畫,轉(zhuǎn)場動(dòng)畫實(shí)現(xiàn)就是如此。
- (void)presentViewControllerWithTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UINavigationController *navVc = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
YMCircleFromViewController * fromVc = navVc.viewControllers.lastObject;
UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVc.view];
UIBezierPath *startCircle = [UIBezierPath bezierPathWithOvalInRect:fromVc.presentBtn.frame];
// sqrtf 求平方根函數(shù) pow求次方函數(shù),這里的意思是求X的2次方,要是pow(m,9)就是求m的9次方
CGFloat radius = sqrtf(pow(containerView.frame.size.width, 2) + pow(containerView.frame.size.height, 2));
UIBezierPath *endCircle = [UIBezierPath bezierPathWithArcCenter:containerView.center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = endCircle.CGPath;
toVc.view.layer.mask = maskLayer;
//創(chuàng)建路徑動(dòng)畫
CABasicAnimation * maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
maskLayerAnimation.delegate = self;
//動(dòng)畫是加到layer上的,所以必須為CGPath,再將CGPath橋接為OC對(duì)象
maskLayerAnimation.fromValue = (__bridge id)(startCircle.CGPath);
maskLayerAnimation.toValue = (__bridge id)((endCircle.CGPath));
maskLayerAnimation.duration = [self transitionDuration:transitionContext];
//速度控制函數(shù)
maskLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[maskLayerAnimation setValue:transitionContext forKey:@"transitionContext"];
// 添加動(dòng)畫
[maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
}
再看一個(gè)簡單的Pop/Push的開關(guān)門動(dòng)畫
//開門動(dòng)畫
- (void) {
YMOpenViewController *vc = [YMOpenViewController new];
self.navigationController.delegate = vc;
[self.navigationController pushViewController:vc animated:YES];
}
//YMOpenViewController 遵守UINavigationControllerDelegate協(xié)議,并實(shí)現(xiàn)此代理方法
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
//通過operation判斷是出棧還是入棧
if (operation == UINavigationControllerOperationPush) {
self.animation.isPop = NO;
} else if (operation == UINavigationControllerOperationPop) {
self.animation.isPop = YES;
}
return self.animation;
}

Demo地址再發(fā)一遍 https://github.com/YanLYM/YMTransitionDemo
至此,presentation-dismissal的自定義轉(zhuǎn)場動(dòng)畫基本做法已講述完畢,下篇一起學(xué)習(xí)UINavigationController 中 push 和 pop 以及 UITabBarController 中切換 Tab的轉(zhuǎn)場。


