iOS学习笔记-自定义过渡动画
这篇笔记翻译自raywenderlick网站的过渡动画的一篇文章,原文用的swift,由于考虑到swift版本变动以及一些语法兼容问题,这里我还是用Objective-C进行了改写,没有逐字翻译,加了部分自己的理解。原文链接Creating Custom UIViewController Transitions。过渡动画有些地方也是翻译成转场动画,即从一个视图控制器切到另一个视图控制器,本文以过渡来译。
1 前言
iOS自身就提供了很多针对UIViewController的过渡动画,比如Cover Vertically(从下往上弹出效果),Cross Dissolve(淡入淡出效果),Partial Curl(书卷翻页效果)等。如图1就是本文用到的示例中的iOS原生的Cover Vertically效果的展示。

为了自己的APP更有个性,自带的效果往往不够酷炫,所以需要自定义过渡动画,通过这篇文章,我们会GET到下面几个技能:
- 过渡动画API的构建。
- 使用自定义的过渡动画来present和dismiss一个视图控制器。present过渡会在应用视图层级结构中添加一个新的视图控制器,而dismiss过渡会从层级结构中删除一个或多个视图控制器。
- 学会使用交互式过渡动画。
在我们开始的示例代码中,还没有加入自定义过渡动画,已经有的内容是一个PageViewController,里面装载的为CardViewController(内容为一个UIView+一个Label用于展示图片描述),点击CardViewController里面的卡片,会切换到RevealViewController(包含一个Label展示图片名字,一个Image View展示宠物图片,一个按钮用于返回到卡片视图)。而我们最终要达到的效果如图2所示:

2 过渡动画API探究
过渡动画API涉及到的一些角色如图3所示,下面分开介绍:

2.1 过渡动画API中的角色
本节内容对过渡动画API中的各个角色进行说明,包含的角色参照图3。
2.1.1 过渡动画代理(Transitioning Delegate)
每个View Controller都有一个transitionDelegate属性,这个代理实现了UIViewControllerTransitioningDelegate协议。
每当你要present或者dismiss一个View Controller的时候,UIKit会去过渡动画代理中查询需要使用的动画效果。实际项目中,我们可以设置代理为自定义的类来返回我们需要的自定义的动画效果。
2.1.2 动画控制器(Animation Controller)
动画控制器是实现了UIViewControllerAnimatedTransitioning协议的用于执行过渡动画的对象。
2.1.3 过渡动画上下文对象(Transitioning Context)
上下文对象实现了UIViewControllerContextTransitioning协议,在动画过程中是至关重要的,它封装了所有的参与过渡动画的View Controllers的信息。不过我们不用写代码实现它,在动画控制器里面,过渡动画执行的时候,我们的函数会接收到一个上下文对象作为参数并从中获取相关View Controller的信息。
2.2 过渡动画流程
- 你触发一个过渡动作。可以通过编码或者segue来触发。
- UIKit询问要过渡到的目的视图控制器它是否有自定义的过渡动画代理。如果没有,则UIKit将使用iOS自带的过渡动画。
- 然后,UIKit通过过渡动画代理,获取到动画控制器。比如通过
animationControllerForPresentedController(_:presentingController:sourceController:)方法获取到动画控制器,如果返回空,则使用默认的动画控制器。
- 然后,UIKit通过过渡动画代理,获取到动画控制器。比如通过
- 一旦找到了动画控制器,UIKit构建上下文对象。
- 接着,UIKit通过动画控制器的
transitionDuration(_:)方法获取动画执行时长。
- 接着,UIKit通过动画控制器的
- 再接着调用动画控制器的
animateTransition(_:)完成过渡动画。
- 再接着调用动画控制器的
- 最后动画控制器调用上下文对象的
completeTransition(_:)方法指示动画完成。图4是官方文档的一个过渡动画的API角色示意图。
- 最后动画控制器调用上下文对象的

2.3 实现Presentation过渡动画
我们总共要实现三个动画效果,一个是Presentation过渡动画,一个是dismiss过渡动画,另外还有一个交互动画。
Presentation的效果主要如下:
- 点击卡片的时候,卡片翻转显示第二个视图,且第二个视图初始大小跟卡片大小一样。
- 第二个视图放大至整个屏幕大小。
2.3.1 创建Presentation动画控制器
我们创建一个名为FlipPresentAnimationController的类来完成Presentation动画效果,这个类在我们上面说的角色中就是动画控制器。
核心代码如下,代码中有注解:
/*设置动画时长函数*/
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 2.0;
}
/*执行动画的函数*/
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
//1 上下文对象transitionContext包含了参与过渡动画的视图
// 和视图控制器信息,可以通过对应的参数获取。
CardViewController *fromVC = (CardViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewKey];
UIView *containerView = [transitionContext containerView];
RevealViewController *toVC = (RevealViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewKey];
//2 设置过渡目的视图的初始大小和结束大小。
// 初始大小为第一个视图的卡片的大小,结束大小为整个屏幕大小。
BOOL hasViewForKey = [transitionContext respondsToSelector:@selector(viewForKey:)];
UIView *fromView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextFromViewKey] : fromVC.view;
UIView *toView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextToViewKey] : toVC.view;
CGRect initialFrame = self.originFrame;
CGRect finalFrame = hasViewForKey? toView.frame : [transitionContext finalFrameForViewController:toVC];
//3 获取一个目的视图的一个快照。设置初始frame为initFrame。
UIView *snapshot = [toView snapshotViewAfterScreenUpdates:YES];
snapshot.frame = initialFrame;
snapshot.layer.cornerRadius = 25;
snapshot.layer.masksToBounds = YES;
//4 containerView加入目的视图和快照视图,并先隐藏目的视图。
// 我们的动画都在containerView来实现。
[containerView addSubview:toView];
[containerView addSubview:snapshot];
toView.hidden = YES;
//5 设置动画视角,将快照视图先沿Y轴旋转到PI/2的位置。
[AnimationHelper persipectiveTransformForContainerView:containerView];
snapshot.layer.transform = [AnimationHelper yRotation:M_PI_2];
CGFloat duration = [self transitionDuration:transitionContext];
[UIView animateKeyframesWithDuration:duration delay:0 options:UIViewKeyframeAnimationOptionCalculationModeCubic animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0/3 animations:^{
//6 将第一个视图旋转到-PI/2的位置,方向是顺时针
fromView.layer.transform = [AnimationHelper yRotation:-M_PI_2];
}];
[UIView addKeyframeWithRelativeStartTime:1.0/3 relativeDuration:1.0/3 animations:^{
//7 将快照视图从PI/2的位置旋转到轴线位置,也是顺时针。正好接上6的旋转效果。
snapshot.layer.transform = [AnimationHelper yRotation:0.0];
}];
[UIView addKeyframeWithRelativeStartTime:2.0/3 relativeDuration:1.0/3 animations:^{
//8 将快照视图的frame放大至整个屏幕。
snapshot.frame = finalFrame;
}];
} completion:^(BOOL finished){
toView.hidden = NO; //显示目的视图
fromView.layer.transform = [AnimationHelper yRotation:0.0]; //恢复第一个视图的位置
[snapshot removeFromSuperview]; //移除快照视图
[transitionContext completeTransition:![transitionContext transitionWasCancelled]]; //通知UIKit动画执行完成
}
];
}
额外说明几点:
- 注释2这段代码跟原文的swift的有点不一样,直接通过
transitionContext viewControllerForKey:UITransitionContextToViewKey等函数取到的View Controller发现是nil,这样就没法取到动画过程中的视图信息。而通过transitionContext viewForKey:UITransitionContextToViewKey取到的视图是正常的,看网上资料说可能是ios8的BUG,没有确切资料可以确认,如果是其他设置问题,麻烦大虾们告知一下。
- 注释2这段代码跟原文的swift的有点不一样,直接通过
- 关于旋转方向的问题,通过上一篇笔记我们总结了三维视图中沿Y轴旋转的正反方向,正方向为逆时针。因此注释5中我们的快照视图显示逆时针的转到了PI/2的位置,而注释6会先将第一个视图转到-PI/2的位置,动画中的旋转方向是以距离最近来旋转,因此第一个视图会顺时针旋转PI/2,然后快照视图也是顺时针旋转PI/2,最后再试快照视图放大到整个屏幕。
- 最后的
completeTransition方法调用是必须的,如果不调用的话,动画结束后目的视图将无法接受事件响应。
- 最后的
2.3.2 连接动画控制器
在我们的CardViewController中加入动画控制器初始化代码。这里的CardViewController实现了UIViewControllerTransitioningDelegate协议,我们要设置目的控制器的transitionDelegate为CardViewController。并实现代理的方法返回我们刚刚创建的动画控制器。代码如下:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
self.flipPresentAnimationController.originFrame = self.cardView.frame;
return self.flipPresentAnimationController;
}
// 在CardViewController的prepareSegue方法中,设置了transitionDelegate。
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
......
revealViewController.transitioningDelegate = self;
}
2.4 实现dismiss过渡动画
dismiss的过渡动画原理类似,不过多介绍了,实现功能是:
- 第二个视图的图片先缩小到第一个视图的卡片大小。
- 两个视图先后翻转,最终回到初始位置。
代码如下:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
CardViewController *fromVC = (CardViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewKey];
UIView *containerView = [transitionContext containerView];
RevealViewController *toVC = (RevealViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewKey];
BOOL hasViewForKey = [transitionContext respondsToSelector:@selector(viewForKey:)];
UIView *fromView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextFromViewKey] : fromVC.view;
UIView *toView = hasViewForKey ? [transitionContext viewForKey:UITransitionContextToViewKey] : toVC.view;
CGRect initialFrame = fromView.frame;
CGRect finalFrame = self.destinationFrame;
UIView *snapshot = [fromView snapshotViewAfterScreenUpdates:YES];
snapshot.frame = initialFrame;
snapshot.layer.cornerRadius = 25;
snapshot.layer.masksToBounds = YES;
[containerView addSubview:toView];
[containerView addSubview:snapshot];
fromView.hidden = YES;
[AnimationHelper persipectiveTransformForContainerView:containerView];
toView.layer.transform = [AnimationHelper yRotation:-M_PI_2];
CGFloat duration = [self transitionDuration:transitionContext];
[UIView animateKeyframesWithDuration:duration delay:0 options:UIViewKeyframeAnimationOptionCalculationModeCubic animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0/3.0 animations:^{
snapshot.frame = finalFrame;
}];
[UIView addKeyframeWithRelativeStartTime:1.0/3.0 relativeDuration:1.0/3.0 animations:^{
snapshot.layer.transform = [AnimationHelper yRotation:M_PI_2];
}];
[UIView addKeyframeWithRelativeStartTime:2.0/3.0 relativeDuration:1.0/3.0 animations:^{
toView.layer.transform = [AnimationHelper yRotation:0.0];
}];
} completion:^(BOOL finished){
fromView.hidden = NO;
[snapshot removeFromSuperview];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}
];
}
当然,也少不了要在代理类中关联好dismiss的动画控制器。
2.5 实现交互动画
2.5.1 交互动画示例
iPhone上面的设置APP就是交互动画的一个很典型的例子,如图5所示,从左边缘开始滑动,过渡动画的进度是跟随你的手指滑动的位置来确定的(比如坐标X超过了多少则表示切换到下一个视图,否则切回上一个视图。

2.5.2 交互动画原理
交互动画通过交互控制器来控制,为了实现交互动画,过渡动画代理需要额外提供一个交互控制器。交互控制器只要实现了UIViewControllerInteractiveTransitioning协议即可,它响应触控事件,通过交互控制器,动画会随着手势拖动逐渐展开而不是像之前那样直接执行完毕。
iOS提供了一个UIPercentDrivenInteractiveTransition类,它实现了UIViewControllerInteractiveTransitioning协议,我们在例子中要用到这个类。
2.5.3 创建交互过渡动画
创建交互动画代码如下,我们需要添加拖动事件响应,在处理事件响应的函数handleGesture中,我们根据当前手势状态和所在的位置来进行处理。注意到gestureRecognizer.view是对应的目的视图也就是RevealViewController对应的View。而它的superview则是UITransitionView这个视图。
- (void)wireToViewController:(UIViewController *)viewController {
self.viewController = viewController;
[self prepareGestureRecognizerInView:viewController.view];
}
- (void)prepareGestureRecognizerInView:(UIView *)view {
UIScreenEdgePanGestureRecognizer *gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action: @selector(handleGesture:)];
gesture.edges = UIRectEdgeLeft;
[view addGestureRecognizer:gesture];
}
- (void)handleGesture:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
//1 获取手势当前的坐标点
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
CGFloat progress = (translation.x / 200);
progress = fminf(fmaxf(progress, 0.0), 1.0);
switch (gestureRecognizer.state) {
//2 开始手势,设置开始交互的标识,开始触发dismissal操作。
case UIGestureRecognizerStateBegan:
self.interactionInProgress = YES;
[self.viewController dismissViewControllerAnimated:YES completion:nil];
Break;
//3 手势拖动,判断当前的手势横轴坐标是否大于100,大于100则设置过渡动画完成。
case UIGestureRecognizerStateChanged:
self.shouldCompleteTransition = progress > 0.5;
[self updateInteractiveTransition:progress];
Break;
//4 手势取消,设置交互状态为NO,并取消交互动画。
case UIGestureRecognizerStateCancelled:
self.interactionInProgress = NO;
[self cancelInteractiveTransition];
Break;
//5 手势结束,根据进度来判断是取消还是完成交互动画。
case UIGestureRecognizerStateEnded:
self.interactionInProgress = NO;
if (!self.shouldCompleteTransition) {
[self cancelInteractiveTransition];
} else {
[self finishInteractiveTransition];
}
default:
NSLog(@"Unsupported");
break;
}
在CardViewController中需要加入对应代码才能呈现交互动画,加入代码如下:
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
return self.swipeInteractionControllers.interactionInProgress ? self.swipeInteractionControllers : nil;
}
/* 在CardViewController的prepareSegue方法中,
设置了transitionDelegate,加入交互动画事件捕获。*/
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
......
revealViewController.transitioningDelegate = self;
[self.swipeInteractionControllers wireToViewController:revealViewController];
}
3 项目文件截图

4 参考资料
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权
iOS学习笔记-自定义过渡动画的更多相关文章
- iOS学习笔记09-核心动画CoreAnimation
http://www.cnblogs.com/liutingIOS/p/5368536.html 一.CALayer CALayer包含在QuartzCore框架中,具有跨平台性,在iOS中使用Cor ...
- iOS学习笔记10-UIView动画
上次学习了iOS学习笔记09-核心动画CoreAnimation,这次继续学习动画,上次使用的CoreAnimation很多人感觉使用起来很繁琐,有没有更加方便的动画效果实现呢?答案是有的,那就是UI ...
- iOS学习笔记-自己动手写RESideMenu
代码地址如下:http://www.demodashi.com/demo/11683.html 很多app都实现了类似RESideMenu的效果,RESideMenu是Github上面一个stars数 ...
- iOS学习笔记-精华整理
iOS学习笔记总结整理 一.内存管理情况 1- autorelease,当用户的代码在持续运行时,自动释放池是不会被销毁的,这段时间内用户可以安全地使用自动释放的对象.当用户的代码运行告一段 落,开始 ...
- iOS学习笔记总结整理
来源:http://mobile.51cto.com/iphone-386851_all.htm 学习IOS开发这对于一个初学者来说,是一件非常挠头的事情.其实学习IOS开发无外乎平时的积累与总结.下 ...
- iOS学习笔记之ARC内存管理
iOS学习笔记之ARC内存管理 写在前面 ARC(Automatic Reference Counting),自动引用计数,是iOS中采用的一种内存管理方式. 指针变量与对象所有权 指针变量暗含了对其 ...
- IOS学习笔记(四)之UITextField和UITextView控件学习
IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...
- IOS学习笔记06---C语言函数
IOS学习笔记06---C语言函数 -------------------------------------------- qq交流群:创梦技术交流群:251572072 ...
- [置顶] iOS学习笔记47——图片异步加载之EGOImageLoading
上次在<iOS学习笔记46——图片异步加载之SDWebImage>中介绍过一个开源的图片异步加载库,今天来介绍另外一个功能类似的EGOImageLoading,看名字知道,之前的一篇学习笔 ...
随机推荐
- centeros7的redis-cli命令不生效解决方法(亲测)
如果你已经安装了redis服务器,并且已经启动,但是redis-cli命令无法生效,分析,命令未加入环境变量.那就给redis命令加入环境变量中: 注意点:redis安装目录会有不同,注意下面的PAT ...
- ubantu16.04安装配置samba服务(原创)
1.安装samba服务 $ sudo apt-get install samba samba-common$ sudo apt-get install smbclient 如果你开启了防火墙,关闭: ...
- 使用CreateRemoteThread把代码远程注入指定exe执行
由于本人也是新手,如果有朋友不懂windows api相关知识,我相信查阅书籍或者百度会比我说有帮助的多,下面就我所做简单复述一下过程,欢迎指正缺点. 效果图示如下: 做的这个例子首先是创建了一个MF ...
- (3)三剑客之sed
(1)基本介绍 1) 工作流程:sed每次处理一行内容,处理时,把当前处理的行存储在临时缓存区,称为模式空间,接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕,直到内容处理完毕2 ...
- 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统
http://www.tuicool.com/articles/NfyqQr 本节主要知识点是easyui 的手风琴加树结构做菜单导航 有园友抱怨原来菜单非常难看,但是基于原有树形无限级别的设计,没有 ...
- 基于tinkphp3.2获取openid
<?php namespace Home\Controller; use Think\Controller; /** * 基础 */ class BaseController extends C ...
- UVALive 3882.And Then There Was One-约瑟夫问题(递推)
And Then There Was One Time limit: 3.000 seconds Let’s play a stone removing game. Initially, n ston ...
- Java学习之路(书籍推荐)
一.基础类 1.<Thinking in java>(阅读2遍),入门第一位是建立正确的概念 2.<Core Java>这本书更贴近实践,更多API的介绍,同样,更新也更频繁. ...
- Codeforces 855E - Salazar Slytherin's Locket
855E - Salazar Slytherin's Locket 题意 给出一个区间,问这个区间内有多少个数满足,将这个数转化为某个进制后,所有数的数量都为偶数. 分析 谁能想到 数位DP 的裸题竟 ...
- 二分图匹配【p2147】课程
Description n个学生去p个课堂,每一个学生都有自己的课堂,并且每个学生只能去一个课堂,题目要求能够安排每一个课堂都有人吗? Input 第一行是测试数据的个数, 每组测试数据的开始分别是p ...