iOS中推出控制器的方式有两种:push和present,iOS的push动画基本上已经成为苹果的一个标志,最好不要自定义,不然和系统的动画不一样会显得不和谐。
关于present,更多的可参考:present和dismiss。
下面介绍如何自定义present方式的转场动画。
1. UIViewControllerTransitioningDelegate协议
想自定义转场动画的VC必须遵守UIViewControllerTransitioningDelegate协议,实现协议的如下方法:
- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [EOCPresentAnimator new];
}
- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed {
return [EOCDismissAnimator new];
}
- (nullable id )interactionControllerForDismissal:(id )animator {
return interactiveTransition;
}
解释:
- 方法1是present的界面添加动画,返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议。
- 方法2是为dismiss的界面添加动画,返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议。
- 方法3是控制转场进度的类,返回的对象要遵守UIViewControllerInteractiveTransitioning协议。
系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议,我们直接使用它的子类。
2. 自定义动画类
接下来我们自定义动画类,遵守UIViewControllerAnimatedTransitioning协议,实现协议的两个方法,如下:
PresentAnimator.m文件:
#import "EOCPresentAnimator.h"
@implementation EOCPresentAnimator
//时间
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext {
return 2.f;
}
//动作 系统会自己调用
- (void)animateTransition:(id )transitionContext {
//上下文对象包含了全部信息
//获取容器View
UIView *containerView = transitionContext.containerView;
//获取到toView:也就是说从ViewCtrlA跳转到ViewCtrlB,toView是ViewCtrlB.view
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
[containerView addSubview:toView];
//rect范围的偏移量,大于0在右半边,下边
CGRect frame = CGRectOffset(toView.frame, 0.f, [UIScreen mainScreen].bounds.size.height);
toView.frame = frame;
[UIView animateWithDuration:2.f animations:^{
toView.frame = CGRectOffset(toView.frame, 0.f, -[UIScreen mainScreen].bounds.size.height);
} completion:^(BOOL finished) {
//结束上下文
[transitionContext completeTransition:YES];
}];
}
@end
DismissAnimator.m文件:
#import "EOCDismissAnimator.h"
@implementation EOCDismissAnimator
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext {
return 2.f;
}
- (void)animateTransition:(id )transitionContext {
UIView *containerView = transitionContext.containerView;
//获取到toView:从ViewCtrlB dismiss 到ViewCtrlA fromView是B, toView是A
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
CGRect finalFrame = CGRectOffset(fromView.frame, 0.f, [UIScreen mainScreen].bounds.size.height);
//containerView里面有fromView了
//把toView放到最下面
[containerView insertSubview:toView atIndex:0];
[UIView animateWithDuration:2.f animations:^{
fromView.frame = finalFrame;
} completion:^(BOOL finished) {
//它肯定实现了移除fromView的操作 取消就不完成
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
//自己移除fromView不行,如果不结束转场,transitionView还在
//[fromView removeFromSuperview];
}];
}
@end
解释:
- - (void)animateTransition:(id
)transitionContext;方法会在从一个VC跳转到另一个VC的时候,系统自动调用。 - 上个方法的参数transitionContext(转场上下文)是转场的中间人,里面保存了fromVC、toVC、containerView以及completeTransition:方法等信息。
动画类创建完成之后,我们在animationControllerForPresentedController:方法和animationControllerForDismissedController:方法里面传入两个动画对象,如下:
#pragma mark - UIViewControllerTransitioningDelegate
//为present的界面添加动画, 返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议
- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [EOCPresentAnimator new];
}
//为dismiss的界面添加动画, 返回的动画对象要遵守UIViewControllerAnimatedTransitioning协议
- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed {
return [EOCDismissAnimator new];
}
调用:
EOCNextViewController *nextViewCtrl = [[EOCNextViewController alloc] init];
nextViewCtrl.transitioningDelegate = self;
[self presentViewController:nextViewCtrl animated:YES completion:nil];
效果图:
下面有个新需求,如何在灰色界面,通过下滑手势dismiss到上一个界面,这里我们就需要用到interactionControllerForDismissal:方法了。
#pragma mark - UIViewControllerTransitioningDelegate
//控制转场进度的类, 返回的对象要遵守UIViewControllerInteractiveTransitioning协议
//系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议, 我们直接使用它的子类
- (nullable id )interactionControllerForDismissal:(id )animator {
return interactiveTransition;
}
这个方法需要返回一个遵守UIViewControllerInteractiveTransitioning协议的对象,由于系统的类UIPercentDrivenInteractiveTransition已经遵守了这个协议,我们直接使用它的子类,代码如下:
EOCInteractiveTransition.h文件
// 控制转场进度的类
#import
@interface EOCInteractiveTransition : UIPercentDrivenInteractiveTransition
- (void)transitionToViewController:(UIViewController *)toViewController;
@end
EOCInteractiveTransition.m文件
#import "EOCInteractiveTransition.h"
@interface EOCInteractiveTransition () {
UIViewController *presentedViewController;
BOOL shouldComplete; //是否拖拽了一半以上
}
@end
@implementation EOCInteractiveTransition
- (void)transitionToViewController:(UIViewController *)toViewController {
presentedViewController = toViewController;
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[toViewController.view addGestureRecognizer:panGesture];
}
- (void)panAction:(UIPanGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
[presentedViewController dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged: {
//监听当前滑动的距离
CGPoint transitionPoint = [gesture translationInView:presentedViewController.view];
NSLog(@"transitionPoint %@", NSStringFromCGPoint(transitionPoint));
CGFloat ratio = transitionPoint.y/[UIScreen mainScreen].bounds.size.height;
NSLog(@"ratio: %f", ratio);
if (ratio >= 0.5) {
shouldComplete = YES;
} else {
shouldComplete = NO;
}
[self updateInteractiveTransition:ratio];
}
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
if (shouldComplete) {
[self finishInteractiveTransition];
} else {
[self cancelInteractiveTransition];
}
}
break;
default:
break;
}
}
@end
调用:
EOCNextViewController *nextViewCtrl = [[EOCNextViewController alloc] init];
[interactiveTransition transitionToViewController:nextViewCtrl];
nextViewCtrl.transitioningDelegate = self;
[self presentViewController:nextViewCtrl animated:YES completion:nil];
效果图:
注意点:
- 没present的时候,层级结构如下
- present之后,层级结构如下
可以发现:
① present之后多了一个UITransitionView,这个View是专门做转场的。
② present之后并没有把控制器或者view直接盖上去,而是先移除旧的再添加新的,这个和push不一样。
自定义转场动画以及自定义容器动画Demo:自定义转场动画