iOS 动画学习之视图控制器转场动画
一、概述
1.系统会创建一个转场相关的上下文对象,传递到动画执行器的animateTransition:和transitionDuration:方法,同样,也会传递到交互Controller的startInteractiveTransition:方法。如果交互Controller的startInteractiveTransition:首先被调用了,那么交互Controller对象应该在需要的时候,手动的调用animateTransition:方法。非交互Controller而是执行普通的Controller的过渡转场动画时,系统将自动调用动画执行器的animateTransition:方法。
系统会查询视图控制器的transitioningDelegate或导航控制器的delegate代理来决定动画执行器或交互Controller是否应该被应用一个过渡转场效果,transitioningDelegate是UIViewController的新属性,该属性是实现了UIViewControllerTransitioningDelegate协议的对象,导航控制器同样的增加了一些新的委托方法。
UIViewControllerContextTransitioning协议可以被容器Controller接收采用,它是被设计用来实现比系统目前支持的更多复杂的转场效果。目前,导航控制器的push/pops行为和视图控制器的present/dismiss转场效果可以被自定义。转场相关的信息与这些方法相关联绑定着,viewControllerForKey:,initialFrameForViewController:和finalFrameForViewController:方法。系统提供两个Key用来辨识导航控制器执行push/pop和视图控制器执行present/dismiss转场时对应的from视图控制器和to视图控制器。
所有自定义的动画必须在转场程序完成时调用上下文对象的completeTransition:方法。此外,动画应该在上下文中指定的containerView中发生。对于交互转场动画,上下文对象的updateInteractiveTransition:,finishInteractiveTransition和cancelInteractiveTransition:方法应该在交互动画过程中被调用。UIPercentDrivenInteractiveTransition类提供了对UIViewControllerInteractiveTransitioning协议的实现,该类可以被用来交互性驱动任何由动画执行器创建的UIView属性动画。
二、UIViewController进行模态跳转的转场
首先,使用CoreAnimation框架中的CATransition类也可以实现视图控制器的转场动画,前面的博客有过讨论,这里不再重复。presentViewController这个函数使用率可谓是非常高的,默认的转场动画为新的视图控制器从下向上弹出,dismissViewControllerAnimated函数的返回动画则是弹出动画的逆序播放。其实,系统提供了4种转场动画供开发者选择,通过设置将要弹出的UIViewController实例的如下属性:
@property(nonatomic,assign) UIModalTransitionStyle modalTransitionStyle;
UIModalTransitionStyle是一个枚举,如下:
typedef NS_ENUM(NSInteger, UIModalTransitionStyle) { UIModalTransitionStyleCoverVertical = 0, //从下向上弹起 默认项 UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED, //水平翻转 UIModalTransitionStyleCrossDissolve, //渐隐渐现 UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED, //翻页 };
很多时候,上面4种枚举的转场动画样式并不能满足我们的需求,我们可以使用UIViewControllerTransitioningDelegate协议来完全自定义想要的转场动画效果。
首先创建一个类,使其遵守UIViewControllerTransitioningDelegate协议,比如我这里将类名去做TransDelegate,继承自NSObject。在界面跳转时,将要弹出的视图控制器设置如下:
ViewController2 * v2 = [ViewController2 new]; self.transDelegate = [[TransDelegate alloc]init]; v2.transitioningDelegate = self.transDelegate; [self presentViewController:v2 animated:YES completion:nil];
我们先来看UIViewControllerTransitioningDelegate协议中的如下几个函数:
//这个函数用来设置当执行present方法时 进行的转场动画 /* presented为要弹出的Controller presenting为当前的Controller source为源Contrller 对于present动作 presenting与source是一样的 */ - (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 NS_AVAILABLE_IOS(8_0);
我们先来看上面的前两个函数,这两个函数都要返回一个实现了UIViewControllerAnimatedTransitioning协议的对象,UIViewControllerAnimatedTransitioning则用来负责具体的动画展示,例如我们在创建一个命名为AniObject的类,继承自NSObject,使其实现UIViewControllerAnimatedTransitioning协议,在TransDelegate类中实现如下:
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ return [AniObject new]; }
下面我们来实现AniObject类来具体的处理动画效果:
UIViewControllerAnimatedTransitioning协议中有两个函数是必须实现的,如下:
//这个函数用来设置动画执行的时长 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{ return 2; } //这个函数用来处理具体的动画 - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{ //跳转的界面 UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; //最终的位置 CGRect finalRect = [transitionContext finalFrameForViewController:toVC]; //起始位置 toVC.view.frame = CGRectOffset(finalRect, [[UIScreen mainScreen]bounds].size.width, 0); //添加到内容视图 [[transitionContext containerView]addSubview:toVC.view]; //执行动画 [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ toVC.view.frame = finalRect; } completion:^(BOOL finished) { //完成动画 [transitionContext completeTransition:YES]; }]; }
上面我们实现了一个简单的自定义转场动画,将present动画修改成了从右侧滑入,但是dismiss动画依然是默认的从下方划出。效果如下:
下面我们来分析下transitionContext这个对象,这个对象实际上是一个转场上下文,使用它来进行动画的定义和执行:
//容器视图 用来表现动画 @property(nonatomic, readonly) UIView *containerView; //下面是几个只读属性 //是否应该执行动画 @property(nonatomic, readonly, getter=isAnimated) BOOL animated; //是否是可交互的 @property(nonatomic, readonly, getter=isInteractive) BOOL interactive; // This indicates whether the transition is currently interactive. //是否被取消了 @property(nonatomic, readonly) BOOL transitionWasCancelled; //转场风格 @property(nonatomic, readonly) UIModalPresentationStyle presentationStyle; //调用这个函数来更新转场过程的百分比 用于可交互动画的阈值 - (void)updateInteractiveTransition:(CGFloat)percentComplete; //完成可交互的转场交互动作时调用 - (void)finishInteractiveTransition; //取消可交互的转场交互动作时调用 - (void)cancelInteractiveTransition; //转场动画被中断 暂停时调用 - (void)pauseInteractiveTransition; //转场动画完成时调用 - (void)completeTransition:(BOOL)didComplete; //获取转场中的两个视图控制器 /* UITransitionContextViewControllerKey的定义 UITransitionContextFromViewControllerKey //原视图控制器 UITransitionContextToViewControllerKey //跳转的视图控制器 */ - (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key; //直接获取转场中的视图 /* UITransitionContextFromViewKey //原视图 UITransitionContextToViewKey //转场的视图 */ - (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key; //获取视图控制器的初识位置 - (CGRect)initialFrameForViewController:(UIViewController *)vc; //获取视图控制器转场后的位置 - (CGRect)finalFrameForViewController:(UIViewController *)vc;
通过上面的介绍,我们可以使用UIViewControllerContextTransitioning随心所欲的定制转场动画,但是还有一个困难我们无法克服,那就是可以交互的转场动画。我们在使用系统的导航控制器时,右划返回效果对用户体验十分友好,我们下面就来试着将视图控制器的模态跳转设计成类似导航可交互的。
首先我们需要实现TransDelegate类中的如下两个函数:
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ return [AniObject new]; } - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{ //遵守了UIViewControllerInteractiveTransitioning协议的对象 return self.object; }
UIViewControllerInteractiveTransitioning协议用来处理可交互的转场动画的具体表现,需要注意,因为使用的是可交互的转场动画,UIViewControllerAnimatedTransitioning协议中的animateTransition:方法可以空实现。下面我们再创建一个遵守UIViewControllerInteractiveTransitioning协议的类,比如命名为IntObject,上面代码中的self.object即是这个类的示例,
IntObject.h文件如下:
@interface IntObject : NSObject<UIViewControllerInteractiveTransitioning> -(void)updateAniProgress:(CGFloat)progress; -(void)finish; -(void)cancel; @end
IntObject.m文件实现如下:
@interface IntObject() @property(nonatomic,strong)id<UIViewControllerContextTransitioning> context; @end @implementation IntObject //这个函数用来保存transitionContext -(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)updateAniProgress:(CGFloat)progress{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; //最终的位置 CGRect fR = CGRectMake( [UIScreen mainScreen].bounds.size.width*progress, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); frameVC.frame = fR; } //结束转场 -(void)finish{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context completeTransition:YES]; }]; } //取消转场 -(void)cancel{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context cancelInteractiveTransition]; }]; } @end
下面我们来添加手势,在ViewController2类中添加如下代码:
@interface ViewController2 () @property(nonatomic,strong)UIPanGestureRecognizer * pan; @end @implementation ViewController2 - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor redColor]; [self.view addGestureRecognizer:self.pan]; // Do any additional setup after loading the view. } -(void)pan:(UIPanGestureRecognizer *)pan{ CGPoint translatedPoint = [pan translationInView:self.view]; CGFloat persent = translatedPoint.x / [[UIScreen mainScreen]bounds].size.width; if (persent<0) { return; } persent = fabs(persent); IntObject * obj = [(TransDelegate *)self.transitioningDelegate object]; switch (pan.state) { case UIGestureRecognizerStateBegan:{ [self dismissViewControllerAnimated:YES completion:nil]; break; } case UIGestureRecognizerStateChanged:{ //手势过程中,通过updateInteractiveTransition设置pop过程进行的百分比 [obj updateAniProgress:persent]; break; } case UIGestureRecognizerStateEnded:{ //手势完成后结束标记并且判断移动距离是否过半,过则finishInteractiveTransition完成转场操作,否者取消转场操作 if (persent > 0.5) { [obj finish]; }else{ [obj cancel]; } break; } default: break; } } -(UIPanGestureRecognizer *)pan{ if (!_pan) { _pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)]; } return _pan; } @end
手势效果如下:
其实,上面演示的是我们自己创建了一个类来实现UIViewControllerInteractiveTransitioning协议,其实系统也为我们提供一个类:UIPercentDrivenInteractiveTransition类,我们可以直接调用这个类的如下3个函数而不需要我们自己重写了,但是必须实现UIViewControllerAnimatedTransitioning协议中的transitionContext函数来实现动画效果。
- (void)updateInteractiveTransition:(CGFloat)percentComplete; - (void)cancelInteractiveTransition; - (void)finishInteractiveTransition;
其实现原理与我们上面进行完全的自定义是一样的。
三、导航转场动画的自定义
导航转场动画的原理与模态跳转转场动画的原理基本是一致的,不同的我们需要设置UINavigationController实例的delegate为遵守UINavigationControllerDelegate协议的类对象。之后实现如下两个函数:
//设置转场的动画不论是push或pop 返回nil 则使用系统默认的导航转场动画 - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { NSLog(@"sss"); return nil; } //设置可交互的转场动画 - (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{ NSLog(@"aaa"); return nil; }
可以看到 animationControllerForOperation:函数依然需要返回一个遵守了UIViewControllerAnimatedTransitioning协议的对象,使用方式和前面所介绍的模态跳转自定义转场一模一样。UINavigationControllerOperation这个枚举将告知开发者导航所做的操作,如下:
typedef NS_ENUM(NSInteger, UINavigationControllerOperation) { UINavigationControllerOperationNone, //无 UINavigationControllerOperationPush, //push操作 UINavigationControllerOperationPop, //pop操作 };
实现UIViewControllerInteractiveTransitioning协议如下:
@interface IntObject() @property(nonatomic,strong)id<UIViewControllerContextTransitioning> context; @end @implementation IntObject -(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)updateAniProgress:(CGFloat)progress{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; //最终的位置 CGRect fR = CGRectMake( [UIScreen mainScreen].bounds.size.width*progress, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); frameVC.frame = fR; [self.context updateInteractiveTransition:progress]; } -(void)finish{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake([UIScreen mainScreen].bounds.size.width, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context finishInteractiveTransition]; [self.context completeTransition:YES]; }]; } -(void)cancel{ [UIView animateWithDuration:0.2 animations:^{ UIView *frameVC = [self.context viewForKey:UITransitionContextFromViewKey]; frameVC.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); } completion:^(BOOL finished) { [self.context cancelInteractiveTransition]; [self.context completeTransition:NO]; }]; } @end
如此即可以轻松实现可交互的自定义导航动画。
四、UITabBarController的转场动画
UITabbar也可以进行转场动画的自定义,需要设置UITabBarController的delegate并实现协议中的如下两个函数:
//设置非交互的转场动画 - (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController animationControllerForTransitionFromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { } //设置交互的转场动画 - (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController{ }
这两个函数的应用和导航自定义动画基本是一致的,这里就不再列举代码,简单的效果见下图:
五.UIViewControllerTransitioning
1.【协议】UIViewControllerContextTransitioning
1.1 【属性】containerView: UIView 发生转场动画所在的视图,允许在自定义转场时往该视图添加或者移除子视图。
1.2 【属性】animated: Bool 大多数时候这个值是YES/true。对于使用了新的呈现类型UIModalPresentationCustom的自定义转场过渡,即使转场过渡没有以动画的形式进行,系统也将调用animateTransition:方法。
1.3 【属性】interactive: Bool 指示转场是否处于交互状态
1.4 【属性】transitionWasCancelled: Bool 指示转场是否被取消
1.5 【属性】presentationStyle: UIModalPresentationStyle
符合UIViewControllerInteractiveTransitioning协议的可交互Controller(由容器型视图控制器的代理声明持有,或就present呈现方式来说,由transitioningDelegate声明持有)应该在转场交互被忽略(scrubbed),然后要么被取消,要么完成转场时调用下面这些方法。注意一下,如果动画执行器是可被中断的,然后调用finishInteractiveTransition:和cancelInteractiveTransition:来标识此次转场未被中断,将自然的完成或者被取消掉。
1.6 【方法】updateInteractiveTransition(percentComplete: CGFloat)
1.7 【方法】finishInteractiveTransition
1.8 【方法】cancelInteractiveTransition
1.9 【方法】pauseInteractiveTransition 当转场过程被中断且需要被暂停时调用此方法
1.10 【方法】completeTransition(didComplete: Bool) 无论转场在什么时候完成或是被取消,此方法都必须被调用。通常此方法由符合UIViewControllerAnimatedTransitioning协议的转场代理对象调用。纯交互式转场过渡此方法应该在交互Controller中调用。此方法在转场结束时有效地更新内部视图控制器的状态。
1.11 【方法】viewControllerForKey(key: UITransitionContextViewControllerKey) 系统当前只定义了两个Key - UITransitionContextToViewControllerKey 和 UITransitionContextFromViewControllerKey。动画执行器不应直接操作一个视图控制器相关联的视图,应该使用viewForKey:来获取。
1.12 【方法】viewForKey(key: UITransitionContextViewKey) 系统当前只定义了两个Key - UITransitionContextFromViewKey 和 UITransitionContextToViewKey。此方法可能返回nil,代表动画执行器不应操作相关联的视图控制器的视图。
1.13 【属性】targetTransform
1.14 【方法】initialFrameForViewController(vc: UIViewController)、finalFrameForViewController(vc: UIViewController) 当frame无法得知或者未定义时将被设置为CGRectZero。例如,当且仅当fromView在转场结束时将从窗口中移除时,fromViewController的finalFrame将为CGRectZero。另一方面,如果finalFrame不为CGRectZero,那么它必须在转场结束时被认真处理。
2.【协议】UIViewControllerAnimatedTransitioning
2.1 【方法】transitionDuration(transitionContext: UIViewControllerContextTransitioning) 百分比驱动交互式转场中被使用,此外也应用于包含有需要与主动画同步的辅助动画的容器Controllers。?
2.2 【方法】animateTransition(transitionContext: UIViewControllerContextTransitioning) 如果转场是交互式但不是百分比驱动式交互转场时,此方法只能是非公开的。?
【可选方法】
2.3 【方法】interruptibleAnimatorForTransition(transitionContext: UIViewControllerContextTransitioning) 由实现了本协议的对象创建的转场如果是可被中断的,那么可实现此方法。例如,可返回一个UIViewPropertyAnimator的实例。此方法被期望返回与转场相同的实例对象。?
2.4 【方法】ainmationEnded(transitionCompleted: Bool) 当转场结束时,系统会回调此方法。可将此方法当做是转场结束的一个监听方法,处理一些善后事宜。O(∩_∩)O
3.【协议】UIViewControllerInteractiveTransitioning
3.1 【方法】startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning)
【可选方法】
3.2 【属性】completionSpeed:CGFloat
3.3 【属性】completionCurve:UIViewAnimationCurve
3.4 【属性】wantsInteractiveStart 在10.0中,如果一个实现了UIViewControllerAnimatedTransitioning协议的对象被知道是可中断的,可能的去启动它,就好像它是不可交互的,然后中断转场并开始与其交互。这种情况下,实现此方法并返回NO/false。如果未实现则返回默认YES/true。
4.【协议】UIViewControllerTransitioningDelegate
【可选方法】
4.1 【方法】animationControllerForPresentedController(presented: UIViewController, presenting: UIViewController, source: UIViewControlelr)
4.2 【方法】animationControllerForDismissedController(dismissed: UIViewController)
4.3 【方法】interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning)
4.4 【方法】interactionControlelrForDismissal(animator: UIViewControllerAnimatedTransitioning)
4.5 【方法】presentationControllerForPresentedViewController(presented: UIViewController, presenting: UIViewController, source: UIViewController)
5.【类】UIPercentDrivenInteractiveTransition 百分比驱动交互式转场过渡类
5.1 【属性】duration: CGFloat 非交互时间,此时间在转场开始时,执行器的transitionDuration:方法被调用时返回。
5.2 【属性】percentComplete: CGFloat 最后的值由updateInteractiveTransition:指定
5.3 【属性】completionSpeed: CGFloat 默认为1,与转场过渡完成时间((1 - percentComplete) * duration)相对应。必须大于0.0。实际完成的时间与completionSpeed成反比。在cancelInteractiveTransition或finishInteractiveTransition调用之前都可以设置该值,以加速或者减慢整个转场的非交互部分。
5.4 【属性】completionCurve: UIViewAnimationCurve 当交互部分的转场完毕时,此属性可以被设置来标识一个不同的动画曲线。默认是UIViewAnimationCurveEaseInOut。注意,在交互期间的动画时间曲线是Linear直线型。
5.5 【属性】timingCurve: UITimingCurveProvider 对于可中断的动画执行器,在转场被中断后继续进行时可指定一个不同的时间曲线提供者。如果动画执行器不可中断,则此属性被忽略。
5.6 【属性】wantsInteractiveStart: Bool 上面有解释。
5.7 【方法】pauseInteractiveTransition 此方法可暂停一个正在进行中可被中断的动画执行器。这可以确保当转场进入或者退出交互模式时,所有由notifyWhenInteractionChangesUsingBlock:方法设置的blocks被执行。
5.8 【方法】updateInteractiveTransition(percentComplete: CGFloat)、cancelInteractiveTransition、finishInteractiveTransition。