iOS开发中,页面之间的跳转无外乎由UINavigationController
管理的push
或者pop
操作、以及由UIViewController
管理的present
和dismiss
操作,无论何种操作,iOS原生系统都为我们提供了页面之间的基础跳转动画。但是往往在开发中,由于各种功能需求,iOS原生系统提供的跳转动画并不能满足我们的需求,好在iOS早就给我们提供了一套自定义转场动画的解决方案,这篇文章就来详细了解一下转场动画。在了解这篇文章之前,先看看iOS提供的整个转场框架
present/dismiss 首先,我们先来了解一下模态跳转
。开发中,假如在A界面需要模态跳转到B界面,通常会这么写:- (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event {
PresentBViewController *bVC = [[PresentBViewController alloc] init];
[self presentViewController:bVC animated:YES completion:nil ];
}
模态消失当前界面则是这么写:- (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:YES completion:nil ];
}
UIViewControllerTransitioningDelegate 使用模态跳转时,系统已经为我们写好的跳转的动画,而要想自定义模态跳转动画,则需要一个实现UIViewControllerTransitioningDelegate
协议、并实现协议部分方法的对象。在UIViewController.h
头文件中,我们可以发现如下定义:@protocol UIViewControllerTransitioningDelegate ;
@interface UIViewController (UIViewControllerTransitioning )
@property (nullable , nonatomic , weak ) id <UIViewControllerTransitioningDelegate > transitioningDelegate NS_AVAILABLE_IOS (7 _0);
@end
如此,我们则需要定义一个对象,并实现UIViewControllerTransitioningDelegate
协议方法,赋值给将要模态跳转的控制器(PresentBViewController
),如下:@interface PresentAViewController ()
@property (nonatomic , strong ) PresentManager *presentManager;
@end
@implementation PresentAViewController
- (void )viewDidLoad {
[super viewDidLoad];
self .view.backgroundColor = [UIColor blueColor];
}
- (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event {
PresentBViewController *bVC = [[PresentBViewController alloc] init];
self .presentManager = [[PresentManager alloc] init];
bVC.transitioningDelegate = self .presentManager;
[self presentViewController:bVC animated:YES completion:nil ];
}
@end
我们先来看看UIViewControllerTransitioningDelegate
有哪些协议方法:
- (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;
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented
presentingViewController:(nullable UIViewController *)presenting
sourceViewController:(UIViewController *)source;
先来看这两个代理方法:animationControllerForPresentedController:presentingController:sourceController:(UIViewController *)source
和animationControllerForDismissedController:(UIViewController *)dismissed
他们都返回了一个实现UIViewControllerAnimatedTransitioning
协议的对象。UIViewControllerAnimatedTransitioning
协议的方法就是我们实现动画的地方。关于此协议我们等会再说。
再来看如下两个方法:interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator
和interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
,他们都返回了一个实现UIViewControllerInteractiveTransitioning
协议的对象,而此协议主要用于交互式转场动画。关于此协议我们等会再说。
push/pop 在使用UINavigationController
进行页面之间的导航管理时,系统也是默认为我们实现了push
和pop
动画的,而如果想自定义push/pop
动画,则需要一个实现UINavigationControllerDelegate
协议的对象。如下:@interface PushAViewController ()
@property (nonatomic , strong ) PushManager *pushManager;
@end
@implementation PushAViewController
- (void )viewDidLoad {
[super viewDidLoad];
self .view.backgroundColor = [UIColor redColor];
}
- (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event {
PushBViewController *bVC = [[PushBViewController alloc] init];
self .pushManager = [[PushManager alloc] init];
bVC.navigationController.delegate = self .pushManager;
[self .navigationController pushViewController:bVC animated:YES ];
}
@end
UINavigationControllerDelegate 我们再来看看UINavigationControllerDelegate
协议的相关方法:
- (nullable id <UIViewControllerInteractiveTransitioning >)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning >) animationController;
- (nullable id <UIViewControllerAnimatedTransitioning >)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation )operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC;
我们会发现 此两种方法分别返回了实现了UIViewControllerInteractiveTransitioning
协议的对象和实现了UIViewControllerAnimatedTransitioning
协议的对象。我们不难发现,无论是push/pop
还是present/dismiss
真正的执行动画都是一个实现了UIViewControllerAnimatedTransitioning
协议的对象,而真正执行交互式动画的都是一个实现了UIViewControllerInteractiveTransitioning
协议的对象。既然如此,那我们就来研究一下UIViewControllerAnimatedTransitioning
和UIViewControllerInteractiveTransitioning
协议
UIViewControllerAnimatedTransitioning 先来看此协议的方法:
- (NSTimeInterval )transitionDuration:(nullable id <UIViewControllerContextTransitioning >)transitionContext;
- (void )animateTransition:(id <UIViewControllerContextTransitioning >)transitionContext;
此协议的两个方法必须实现。transitionDuration:
方法返回动画执行的时间。通常情况下,系统执行present/dismiss
和push/pop
动画的时间为0.5秒左右。animateTransition
方法就是真正执行动画的地方。这个方法系统会给我们传过来一个实现了UIViewControllerContextTransitioning
协议的对象(转场上下文),在执行动画之前,我们先来了解一下UIViewControllerContextTransitioning
协议。
先来看UIViewControllerContextTransitioning
协议的定义
- (UIView *)containerView;
- (BOOL )isAnimated;
- (BOOL )isInteractive;
- (BOOL )transitionWasCancelled;
- (UIModalPresentationStyle )presentationStyle;
- (void )updateInteractiveTransition:(CGFloat )percentComplete;
- (void )finishInteractiveTransition;
- (void )cancelInteractiveTransition;
- (void )pauseInteractiveTransition NS_AVAILABLE_IOS (10 _0);
- (void )completeTransition:(BOOL )didComplete;
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey )key;
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey )key NS_AVAILABLE_IOS (8 _0);
- (CGAffineTransform )targetTransform NS_AVAILABLE_IOS (8 _0);
- (CGRect )initialFrameForViewController:(UIViewController *)vc;
- (CGRect )finalFrameForViewController:(UIViewController *)vc;
了解完UIViewControllerContextTransitioning
协议后,我们就可以实现自定义转场动画了。
第一节中,我们定义了类PresentManager
并实现了UIViewControllerAnimatedTransitioning
和UIViewControllerTransitioningDelegate
,我们先来看UIViewControllerTransitioningDelegate
协议的具体实现
- (nullable id <UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
self .transitionStyle = TransitionStylePresent;
return self ;
}
- (nullable id <UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController *)dismissed {
self .transitionStyle = TransitionStyleDismiss;
return self ;
}
再来看看UIViewControllerAnimatedTransitioning
协议的具体实现:- (NSTimeInterval )transitionDuration:(nullable id <UIViewControllerContextTransitioning >)transitionContext {
return 1.0 ;
}
- (void )animateTransition:(id <UIViewControllerContextTransitioning >)transitionContext {
UIView *containerView = [transitionContext containerView];
if (!containerView) {
return ;
}
if (self .transitionStyle == TransitionStylePresent) {
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey ];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey ];
UIView *fromView, *toView;
if ([transitionContext respondsToSelector:@selector (viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey ];
toView = [transitionContext viewForKey:UITransitionContextToViewKey ];
}else {
fromView = fromViewController.view;
toView = toViewController.view;
}
fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
toView.frame = [transitionContext finalFrameForViewController:toViewController];
toView.frame = CGRectMake (-toView.frame.size.width, toView.frame.origin.y, toView.frame.size.width, toView.frame.size.height);
[containerView addSubview:fromView];
[containerView addSubview:toView];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toView.frame = CGRectMake (0 , toView.frame.origin.y, toView.frame.size.width, toView.frame.size.height);
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}else {
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey ];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey ];
UIView *fromView, *toView;
if ([transitionContext respondsToSelector:@selector (viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey ];
toView = [transitionContext viewForKey:UITransitionContextToViewKey ];
}else {
fromView = fromViewController.view;
toView = toViewController.view;
}
fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
toView.frame = [transitionContext finalFrameForViewController:toViewController];
[containerView addSubview:toView];
[containerView addSubview:fromView];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromView.frame = CGRectMake (0 , -fromView.frame.size.height, fromView.frame.size.width, fromView.frame.size.height);
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}
我这里只是做一个简单的动画,至于该如何做更复杂的动画,那就开动大家的大脑吧。
同样的,自定义push/pop
动画也是如此,这里不再详解。
交互式动画 在前面,我们一直提到一个协议UIViewControllerInteractiveTransitioning
,实现此协议,我们就能使用交互式转场动画。iOS7为我们提供了已经实现好此协议的类UIPercentDrivenInteractiveTransition
,我们只需继承此类,便可实现交互式动画。为了方便,下面的例子依旧在present/dismiss
转场基础上讲解,我们让PresentManager
继承UIPercentDrivenInteractiveTransition
,并且在PresentBViewController
上添加pan
手势,具体代码如下:
- (nullable id <UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
self .transitionStyle = TransitionStylePresent;
[self addGesture:presented];
self .presentingVC = presented;
return self ;
}
- (nullable id <UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning >)animator {
if (self .interacting) {
return self ;
}
return nil ;
}
- (void )addGesture:(UIViewController *)viewController {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector (panGesture:)];
[viewController.view addGestureRecognizer:pan];
}
- (void )panGesture:(UIPanGestureRecognizer *)gesture {
CGPoint translation = [gesture translationInView:gesture.view];
switch (gesture.state) {
case UIGestureRecognizerStateBegan :
self .interacting = YES ;
[self .presentingVC dismissViewControllerAnimated:YES completion:nil ];
break ;
case UIGestureRecognizerStateChanged : {
CGFloat fraction = translation.x / gesture.view.frame.size.width;
[self updateInteractiveTransition:fraction];
break ;
}
case UIGestureRecognizerStateEnded :
case UIGestureRecognizerStateCancelled : {
self .interacting = NO ;
CGFloat fraction = translation.x / gesture.view.frame.size.width;
if (fraction<0.5 || gesture.state == UIGestureRecognizerStateCancelled ) {
[self cancelInteractiveTransition];
} else {
[self finishInteractiveTransition];
}
break ;
}
default :
break ;
}
}
同样的,push/pop
亦是如此。
UIPresentationController 在第一节,我们说到UIViewControllerTransitioningDelegate
协议的时候,还有个方法没有说,它是iOS8以后才有的,此方法为:- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented
presentingViewController:(nullable UIViewController *)presenting
sourceViewController:(UIViewController *)source
此方法返回一个UIPresentationController
对象,这个对象是做什么的呢?
UIViewController
有一个属性modalPresentationStyle
,我们来看看它有哪些值:typedef NS_ENUM (NSInteger , UIModalPresentationStyle ) {
UIModalPresentationFullScreen = 0 ,
UIModalPresentationPageSheet NS_ENUM_AVAILABLE_IOS (3 _2) __TVOS_PROHIBITED,
UIModalPresentationFormSheet NS_ENUM_AVAILABLE_IOS (3 _2) __TVOS_PROHIBITED,
UIModalPresentationCurrentContext NS_ENUM_AVAILABLE_IOS (3 _2),
UIModalPresentationCustom NS_ENUM_AVAILABLE_IOS (7 _0),
UIModalPresentationOverFullScreen NS_ENUM_AVAILABLE_IOS (8 _0),
UIModalPresentationOverCurrentContext NS_ENUM_AVAILABLE_IOS (8 _0),
UIModalPresentationPopover NS_ENUM_AVAILABLE_IOS (8 _0) __TVOS_PROHIBITED,
UIModalPresentationNone NS_ENUM_AVAILABLE_IOS (7 _0) = -1 ,
};
平常开发中,我们使用最多的就是UIModalPresentationCustom
,如果我们不使用UIModalPresentationCustom
,默认的系统会在我们调用“上下文”的completeTransition
方法后会把fromVC移除掉。
如果我们想在present/pop
执行动画的生命周期过程中,任意的在上下文中插入视图或者更改最终视图的大小等,使用UIPresentationController
便可实现。我们来看UIPresentationController
类的定义:
@property (nullable , nonatomic , readonly , strong ) UIView *containerView;
- (void )containerViewWillLayoutSubviews;
- (void )containerViewDidLayoutSubviews;
- (CGRect )frameOfPresentedViewInContainerView;
- (void )presentationTransitionWillBegin;
- (void )presentationTransitionDidEnd:(BOOL )completed;
- (void )dismissalTransitionWillBegin;
- (void )dismissalTransitionDidEnd:(BOOL )completed;
如此,我们便可在视图周期方法中任意的添加和删除视图,以满足我们的需求。也可以在布局过程中,改变弹出视图的frame。切记,视图的添加和删除都是在上下文的view中进行的。
小结 了解完整个转场动画的框架,我们合理的使用框架中的协议和类,便能尽可能的满足我们的开发需求。至于该如何实现,大家可以动脑了~