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中进行的。
小结 了解完整个转场动画的框架,我们合理的使用框架中的协议和类,便能尽可能的满足我们的开发需求。至于该如何实现,大家可以动脑了~