以下是iOS7之后,苹果开放转场动画接口结构图,分别是UITabBarControllerDelegate 、UIViewControllerTransitiningDelegate、UINavigationControllerDelegate
第一节:ViewController的模态跳转:动画自定义
UIViewControllerTransitioningDelegate
这个函数用来设置调用present
方法和dismiss
方法时,进行的转场动画
/// 执行present方法,进行的转场动画
/// presented:将要弹出的Controller
/// presenting:当前的Controller
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
/// 执行dismiss方法,进行的转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
/// 执行present方法,进行的交互式转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
/// 执行dismiss方法,进行的交互式转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
/// iOS8后提供的新接口 返回UIPresentationController处理转场
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source API_AVAILABLE(ios(8.0));
UIViewControllerAnimatedTransitioning
接口,是自定义转场动画的重点
@protocol UIViewControllerAnimatedTransitioning <NSObject>
/// 返回动画执行的时长
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
/// 自定义转场动画的实现
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
@optional
UIViewControllerContextTransitioning
接口,转场上下文,能获取到转场动画中参数及状态
@protocol UIViewControllerContextTransitioning <NSObject>
/// 容器视图: 用来表现动画
@property(nonatomic, readonly) UIView *containerView;
/// 是否应该执行动画
@property(nonatomic, readonly, getter=isAnimated) BOOL animated;
/// 是否可狡猾
@property(nonatomic, readonly, getter=isInteractive) BOOL interactive;
/// 是否被取消了
@property(nonatomic, readonly) BOOL transitionWasCancelled;
/// 转场的风格
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
/*
可交互转场动画特有的
*/
/// 更新转场过程的百分比,用于可交互动画的阀值
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
/// 完成可交互的转场交互动作时调用
- (void)finishInteractiveTransition;
/// 取消可交互的转场交互动作时调用
- (void)cancelInteractiveTransition;
/// 转场动画被中断、暂停时调用
- (void)pauseInteractiveTransition API_AVAILABLE(ios(10.0));
/// 转场动画完成时调用
- (void)completeTransition:(BOOL)didComplete;
/*
获取转场中两个视图控制器
UITransitionContextViewControllerKey 的定义
UITransitionContextFromViewControllerKey /// 原视图控制器
UITransitionContextToViewControllerKey /// 跳转的视图控制器
*/
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
/*
直接获取转场中的视图
UITransitionContextViewKey 的定义
UITransitionContextFromViewKey /// 原控制器的视图
UITransitionContextToViewKey /// 转场控制器的视图
*/
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key API_AVAILABLE(ios(8.0));
@property(nonatomic, readonly) CGAffineTransform targetTransform API_AVAILABLE(ios(8.0));
/// 获取视图控制器的初始位置
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
/// 获取视图控制器转场后的位置
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
@end
小案例:自定义一个从右边滑入的present转场动画
第一步:
自定义转场动画
,创建PanPresentAnimation实现UIViewControllerAnimatedTransitioning
接口
@interface PanPresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>
@end
@implementation PanPresentAnimation
/// 设置转场动画时间
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return 0.8f;
}
/// 自定义转场动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
/// 获取切入ViewController
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
/// 设置要切入的ViewController的初始位置
/// 实现的效果:从右边滑入的效果
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
toVC.view.frame = CGRectOffset(finalFrame, screenBounds.size.width, 0);
/// 将切入的ViewController的View添加到containerView
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
/// 自动定义动画:弹出动画
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration
delay:0.0
usingSpringWithDamping:0.8
initialSpringVelocity:5
options:UIViewAnimationOptionCurveLinear
animations:^{
toVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
/// 必须要告诉context 切换完成
[transitionContext completeTransition:YES];
}];
}
@end
第二步:
遵守UIViewControllerTransitioningDelegate协议
,返回自定义present转场动画PanPresentAnimation
#import "AViewController.h"
#import "BViewController.h"
#import "PanPresentAnimation"
@interface MainViewController ()<UIViewControllerTransitioningDelegate>
@end
@implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
[button setTitle:@"Click me" forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
-(void) buttonClicked:(id)sender {
BViewController *mvc = [[BViewController alloc] init];
mvc.transitioningDelegate = self;
mvc.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:mvc animated:YES completion:nil];
}
/// 返回自定义的present转场动画对象:BouncePresentAnimation
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [PanPresentAnimation new];
}
@end
可交互转场动画自定义
可交互转场动画,按我的理解就是可以控制转场动画的进度
,比如苹果系统提供的右滑返回上一级一样。
在UIViewControllerTransitioningDelegate协议中有两个可以定义可交互转场的方法:interactionControllerForPresentation
方法很少用,因为下一级界面是不确定的,常用的是interactionControllerForDismissal
,因为上一级界面是确定的。
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
对于UIViewControllerTransitioningDelegate协议中对于dismiss转场动画的方法有以下两个:
一个是不可交互
的普通转场:animationControllerForDismissedController
一个是可交互
的转场:interactionControllerForDismissal
两者的关系苹果官方是这么写的:
(1)interactionControllerForDismissal方法中的
anmatore参数
是由animationControllerForDismissedController方法返回的
(2)如果interactionControllerForDismissal 返回结果是nil
,则是不执行可交互动画
,执行普通的转场动画
(即animationControllerForDismissedController返回定义的动画)
(3)如果要执行可交互动画
,那么也必须要实现animationControllerForDismissedController
方法,并返回一个普通的转场动画,如果animationControllerForDismissedController返回是nil或没实现,那么interactionControllerForDismissal方法是不会执行的
小案例:在上一个案例中继续开发,在presentB界面后,自定义dismiss可交换的转场动画,效果:右滑返回。(案例:A present B,B dismiss A)
第一步:创建自定义可交换动画,
PanInteractiveTransition
实现UIViewControllerInteractiveTransitioning
接口
@interface PanInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
/// 是否处于交互的状态
@property (nonatomic, assign) BOOL interacting;
/// 用于为B界面添加拖动手势
/// @param viewController BController
- (void)forPresentingViewController:(UIViewController *)viewController;
@end
@interface PanInteractiveTransition()
@property (nonatomic, strong) id<UIViewControllerContextTransitioning> context;
@property (nonatomic, strong) UIViewController *presentingViewController;
@end
@implementation PanInteractiveTransition
/// 为B界面添加拖动手势
- (void)forPresentingViewController:(UIViewController *)viewController {
self.presentingViewController = viewController;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handleGesgture:)];
[viewController.view addGestureRecognizer: pan];
}
/// 拖动手势响应
- (void)handleGesgture:(UIPanGestureRecognizer *)pan {
CGPoint translation = [pan translationInView:pan.view.superview];
CGFloat persent = translation.x / [UIScreen mainScreen].bounds.size.width;
if (persent < 0) {
return;
}
persent = fabs(persent);
switch (pan.state) {
/// 手势开始
case UIGestureRecognizerStateBegan:
/// 标记:交互中
self.interacting = YES;
/// 执行dismiss方法
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
break;
/// 拖动中
case UIGestureRecognizerStateChanged: {
/// 更新转场动画进度
[self updateAniProgess:persent];
break;
}
/// 拖动结束
case UIGestureRecognizerStateEnded:
/// 拖动取消、中断
case UIGestureRecognizerStateCancelled: {
/// 标记:交互结束
self.interacting = NO;
/// 如果滑动查过50%,则返回,否则取消返回原点
if (persent > 0.5) {
[self finish];
}else{
[self cancel];
}
break;
}
default:
break;
}
}
/// UIViewControllerInteractiveTransitioning协议方法
/// 触发:开始交互转场时
/// 作用:将两个ViewController中View的添加到视图容器containerView中
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
self.context = transitionContext;
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
[[transitionContext containerView] insertSubview:toVC.view belowSubview:fromVC.view];
}
/// 更新动画状态
- (void)updateAniProgess:(CGFloat)progress {
UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey];
CGRect finalRect = CGRectMake([UIScreen mainScreen].bounds.size.width * progress, 0, frameVC.bounds.size.width, frameVC.bounds.size.height);
frameVC.frame = finalRect;
}
/// 转场结束
- (void)finish {
[UIView animateWithDuration:0.2 animations:^{
UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey];
CGRect finalRect = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, frameVC.bounds.size.width, frameVC.bounds.size.height);
frameVC.frame = finalRect;
} completion:^(BOOL finished) {
[self.context completeTransition:YES];
}];
}
/// 转场取消
- (void)cancel {
[UIView animateWithDuration:0.2 animations:^{
UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey];
CGRect finalRect = CGRectMake(0, 0, frameVC.bounds.size.width, frameVC.bounds.size.height);
frameVC.frame = finalRect;
} completion:^(BOOL finished) {
[self.context cancelInteractiveTransition];
}];
}
@end
第二步:实现
interactionControllerForDismissal
,返回自定义的可交互动画
#import "AController.h"
#import "BController.h"
#import "PanPresentAnimation.h"
#import "PanDimissAnimation.h"
#import "PanInteractiveTransition.h"
@interface AController ()<UIViewControllerTransitioningDelegate>
@property (nonatomic, strong) PanPresentAnimation *presentAnimation;
@property (nonatomic, strong) PanInteractiveTransition *panInteractiveTransition;
@end
@implementation AController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
_presentAnimation = [PanPresentAnimation new];
_panInteractiveTransition = [PanInteractiveTransition new];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
[button setTitle:@"Click me" forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
-(void) buttonClicked:(id)sender {
BController *mvc = [[BController alloc] init];
mvc.transitioningDelegate = self;
mvc.modalPresentationStyle = UIModalPresentationFullScreen;
[self.panInteractiveTransition forPresentingViewController:mvc];
[self presentViewController:mvc animated:YES completion:nil];
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
/// 普通的present转场动画
/// 这里个类实现则写了,与present动画方式是一样的,详情请看最后的demo代码
return self.presentAnimation;
}
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
/// 普通的dimiss转场动画
return [PanDimissAnimation new];
}
-(id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
/// 这里要判断是否交互触发的dismiss方法,如果不是,则返回nil,表示执行普通dimiss转场动画
return self.panInteractiveTransition.interacting ? self.panInteractiveTransition : nil;
}
@end
补充:
可交互转场接口UIViewControllerInteractiveTransitioning,系统还提供了”继承于“ UIViewControllerInteractiveTransitioning的接口UIPercentDrivenInteractiveTransition
。
UIPercentDrivenInteractiveTransition
做的事跟我们上面自定义的PanInteractiveTransition
差不多,主要包括updateInteractiveTransition:
、cancelInteractiveTransition
、finishInteractiveTransition
,有些属性是iOS10之后才引入的,为低版本兼容,就不采用。比如记录是否处于可交互状态wantsInteractiveStart
,而是使用自定义的属性。
但是,但是,但是
,UIPercentDrivenInteractiveTransition
与UIViewControllerInteractiveTransitioning
还是很不一样
的,官方文档是这样描述的:
大体的意思是:
(1)
UIPercentDrivenInteractiveTransition
对象依赖于UIViewControllerTransitioningDelegate
协议返回的动画对象去执行设置动画和执行动画的
(也就是说:UIViewControllerTransitioningDelegate
百分比可交互转场动画UIPercentDrivenInteractiveTransition设置,是在协议方法中animationControllerForPresentedController:
和 animationControllerForDismissedController:
返回对象中设置的,还有一个要注意的地方,请看备注1
)(2)可交互转场进度,用户可以使用
updateInteractiveTransition:
、finishInteractiveTransition
、cancelInteractiveTransition
三个方法控制
备注1:
以animationControllerForDismissedController:
方法为例,执行completeTransition:
方法时就不能写死YES,需要设置成[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
因为交互转场可能取消了,那么就需要返回初始位置
案例:重写上例dismiss可交互返回的动画为例
@interface PanDimissAnimation : NSObject<UIViewControllerAnimatedTransitioning>
@end
@implementation PanDimissAnimation
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.4f;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
NSLog(@"animateTransition: ===== %d",transitionContext.isInteractive);
/// 原VC
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
/// 跳转VC
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
/// 获取初始位置
CGRect initialFrame = [transitionContext initialFrameForViewController:fromVC];
/// 计算转场最后的位置
CGRect finalFrame = CGRectOffset(initialFrame, screenW, 0);
/// 都需要将toVC.view添加到视图容器containerView中,不论dismiss动画还是present动画
/// 将toVC.view 添加到视图容器containerView中
/// 如果不添加也可以,不过效果不好,在切换的时候,会有一小段是白屏
/// 推荐添加,并且要置于底部,否则会覆盖在fromVC.view 上面
[transitionContext.containerView addSubview:toVC.view];
[transitionContext.containerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
/// 设置最后的位置
fromVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
/// 动画完成
/// (1)completeTransition调用如果返回YES,则会将fromVC.view 从ContainView中移除
/// 而这里是否完成通过![transitionContext transitionWasCancelled]赋值,
/// 因为对于可交互动画,是有可能被取消的,那么就需要把fromVC.view复原原来的位置,不能移除
/// (当可交互动画是继承于UIPercentDrivenInteractiveTransition就会出现这种情况)
/// (2)如果是没有可交互的动画,那么直接返回YES也是可以,但是推荐使用第一种方式
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
@end
@interface PanPercentIinteractiveTransition : UIPercentDrivenInteractiveTransition
/// 是否处于交互的状态
@property (nonatomic, assign) BOOL interacting;
/// 用于为B界面添加拖动手势
/// @param viewController BController
- (void)forPresentingViewController:(UIViewController *)viewController;
@end
@interface PanPercentIinteractiveTransition()
@property (nonatomic, strong) UIViewController *presentingViewController;
@end
@implementation PanPercentIinteractiveTransition
/// 用于为B界面添加拖动手势
/// @param viewController BController
- (void)forPresentingViewController:(UIViewController *)viewController {
self.presentingViewController = viewController;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handleGesgture:)];
[viewController.view addGestureRecognizer: pan];
}
/// 拖动手势响应
- (void)handleGesgture:(UIPanGestureRecognizer *)pan {
CGPoint translation = [pan translationInView:pan.view.superview];
CGFloat persent = translation.x / [UIScreen mainScreen].bounds.size.width;
if (persent < 0) {
return;
}
persent = fabs(persent);
NSLog(@"========== persent:%f",persent);
switch (pan.state) {
/// 手势开始
case UIGestureRecognizerStateBegan: {
/// 标记:交互中
self.interacting = YES;
/// 执行dismiss方法
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
break;
/// 拖动中
case UIGestureRecognizerStateChanged: {
NSLog(@"update=====");
/// 更新转场动画进度
[self updateInteractiveTransition:persent];
}
break;
/// 拖动结束
case UIGestureRecognizerStateEnded:
/// 拖动取消、中断
case UIGestureRecognizerStateCancelled: {
/// 标记:交互结束
self.interacting = NO;
/// 如果滑动查过50%,则返回,否则取消返回原点
if (persent > 0.5) {
[self finishInteractiveTransition];
}else{
[self cancelInteractiveTransition];
}
}
break;
default:
break;
}
}
@end
DEMO
Present/Dimiss 代码
链接: https://pan.baidu.com/s/1hkR_mhB2AsOq9hu82n-nOQ 密码: 612k
第二节:导航栏转场动画自定义
由第一节已经知道自定义转场动画的流程及设计的类、接口等,其实导航栏转场与模态转场自定义也是一样的基本原理,不同的是变成设置UINavigationController实例的delegate(
UINavigationControllerDelegate
),其余的都是一样,这里就不展开细讲🙅♂️。
/// 设置转场动画,如果返回nil,则使用系统默认的导航栏转场动画,不论PUSH或POP
/// UINavigationControllerOperation 枚举
/// UINavigationControllerOperationNone, //无
/// UINavigationControllerOperationPush, //push操作
/// UINavigationControllerOperationPop, //pop操作
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC API_AVAILABLE(ios(7.0));
/// 设置可交互动画
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController API_AVAILABLE(ios(7.0));
第三节:tabBarController切换转场动画自定义
UITabBarController也是自定义转场动画的,套路一样,只是delegate不同(
UITabBarControllerDelegate
)
/// 普通转场
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
animationControllerForTransitionFromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC API_AVAILABLE(ios(7.0));
/// 可交互转场
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController API_AVAILABLE(ios(7.0));
解惑:ContainerView
相信你在看文章的时候,一定有个疑惑,到底containerView
是什么鬼?是临时的视图容器么?它跟FromVC.view 与 ToVC.view 之间的视图层级关系
到底是怎么样?(这很重要,很关键,对于自定义转场动画有很大的帮助
)
笔者刚开始的时候也是有很大的疑惑,接下来以 A PUSH B
为例,Demo代码下载地
链接: https://pan.baidu.com/s/1Usk2KtjoLu2PenGusVZ3ig 密码: tceb
从上图可以看到containView是一直存在的(UINavigationController、UITabBarController、UIWindow都是带有一个转场视图UITransitionView,用于展示转场动画及控制器视图的
),所以上文自定义转场动画中提到一定要将toVIewController.view 添加到ContainerView中。
参考文章
WWDC 2013 Session笔记 - iOS7中的ViewController切换
iOS自定义转场动画
【实战】快速集成自定义转场动画&手势驱动
iOS 自定义转场动画浅谈
iOS自定义转场动画(push、pop动画)
iOS 两行代码实现自定义转场动画
使用 UIPercentDrivenInteractiveTransition