代码地址如下:
http://www.demodashi.com/demo/11678.html

这篇笔记翻译自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 过渡动画流程

    1. 你触发一个过渡动作。可以通过编码或者segue来触发。
    1. UIKit询问要过渡到的目的视图控制器它是否有自定义的过渡动画代理。如果没有,则UIKit将使用iOS自带的过渡动画。
    1. 然后,UIKit通过过渡动画代理,获取到动画控制器。比如通过 animationControllerForPresentedController(_:presentingController:sourceController:)方法获取到动画控制器,如果返回空,则使用默认的动画控制器。
    1. 一旦找到了动画控制器,UIKit构建上下文对象。
    1. 接着,UIKit通过动画控制器的 transitionDuration(_:)方法获取动画执行时长。
    1. 再接着调用动画控制器的animateTransition(_:)完成过渡动画。
    1. 最后动画控制器调用上下文对象的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动画执行完成
}
];
}

额外说明几点:

    1. 注释2这段代码跟原文的swift的有点不一样,直接通过transitionContext viewControllerForKey:UITransitionContextToViewKey等函数取到的View Controller发现是nil,这样就没法取到动画过程中的视图信息。而通过transitionContext viewForKey:UITransitionContextToViewKey取到的视图是正常的,看网上资料说可能是ios8的BUG,没有确切资料可以确认,如果是其他设置问题,麻烦大虾们告知一下。
    1. 关于旋转方向的问题,通过上一篇笔记我们总结了三维视图中沿Y轴旋转的正反方向,正方向为逆时针。因此注释5中我们的快照视图显示逆时针的转到了PI/2的位置,而注释6会先将第一个视图转到-PI/2的位置,动画中的旋转方向是以距离最近来旋转,因此第一个视图会顺时针旋转PI/2,然后快照视图也是顺时针旋转PI/2,最后再试快照视图放大到整个屏幕。
    1. 最后的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 参考资料

代码地址如下:
http://www.demodashi.com/demo/11678.html

注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权

iOS学习笔记-自定义过渡动画的更多相关文章

  1. iOS学习笔记09-核心动画CoreAnimation

    http://www.cnblogs.com/liutingIOS/p/5368536.html 一.CALayer CALayer包含在QuartzCore框架中,具有跨平台性,在iOS中使用Cor ...

  2. iOS学习笔记10-UIView动画

    上次学习了iOS学习笔记09-核心动画CoreAnimation,这次继续学习动画,上次使用的CoreAnimation很多人感觉使用起来很繁琐,有没有更加方便的动画效果实现呢?答案是有的,那就是UI ...

  3. iOS学习笔记-自己动手写RESideMenu

    代码地址如下:http://www.demodashi.com/demo/11683.html 很多app都实现了类似RESideMenu的效果,RESideMenu是Github上面一个stars数 ...

  4. iOS学习笔记-精华整理

    iOS学习笔记总结整理 一.内存管理情况 1- autorelease,当用户的代码在持续运行时,自动释放池是不会被销毁的,这段时间内用户可以安全地使用自动释放的对象.当用户的代码运行告一段 落,开始 ...

  5. iOS学习笔记总结整理

    来源:http://mobile.51cto.com/iphone-386851_all.htm 学习IOS开发这对于一个初学者来说,是一件非常挠头的事情.其实学习IOS开发无外乎平时的积累与总结.下 ...

  6. iOS学习笔记之ARC内存管理

    iOS学习笔记之ARC内存管理 写在前面 ARC(Automatic Reference Counting),自动引用计数,是iOS中采用的一种内存管理方式. 指针变量与对象所有权 指针变量暗含了对其 ...

  7. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  8. IOS学习笔记06---C语言函数

    IOS学习笔记06---C语言函数 --------------------------------------------  qq交流群:创梦技术交流群:251572072              ...

  9. [置顶] iOS学习笔记47——图片异步加载之EGOImageLoading

    上次在<iOS学习笔记46——图片异步加载之SDWebImage>中介绍过一个开源的图片异步加载库,今天来介绍另外一个功能类似的EGOImageLoading,看名字知道,之前的一篇学习笔 ...

随机推荐

  1. .net core 2.0 报错:error NU1102: Unable to find package ...

    原文地址:传送门 这种是nuget无法还原的问题.解决问题的方法: 在项目文件所在的目录下创建文件:NuGet.Config 里面内容: <?xml version="1.0" ...

  2. [ThinkPHP] 从一个表中获得栏目对应的ID,从另一个表获得属于这些栏目的文章

    public function index(){ $cate = 1; $query = M('Cate')->field('id')->where(array('id'=>$cat ...

  3. UVa1599 Ideal Path(双向bfs+字典序+非简单图的最短路+队列判重)

    题目大意: 对于一个n个房间m条路径的迷宫(Labyrinth)(2<=n<=100000, 1<=m<=200000),每条路径上都涂有颜色,颜色取值范围为1<=c&l ...

  4. 洛谷——P1689 方程求解

    P1689 方程求解 题目描述 给一个方程,形如X+Y=Z或X-Y=Z.给出了其中两个未知数,请求出第三个数.未知数用‘?’表示,等式中也许会出现一些多余的空格. 输入输出格式 输入格式: 一行,方程 ...

  5. RabbitMQ生产部署指南

    像RabbitMQ这样的数据服务通常有许多可调参数.一些配置对开发有很大的意义,但并不适合生产,本指南旨在为此提供帮助 虚拟主机 例如,在单租户环境中,当您的RabbitMQ集群专门为生产中的单个系统 ...

  6. AtCoder - 2705 Yes or No

    Problem Statement You are participating in a quiz with N+M questions and Yes/No answers. It's known ...

  7. 【Treap】BZOJ1588-[HNOI2002]营业额统计

    [题目大意][借用别人的概括]给出一个n个数的数列a,对于第i个元素ai定义fi=min(abs(ai-aj)),(1<=j<i),其中f1=a1.输出sum(fi) (1<=i&l ...

  8. scrapy初探

    一  创建scrapy项目 运行命令: scrapy startproject 项目名称 目录结构 二  定义Item容器 Item是保存爬取到数据的容器,其使用方法和python字典类似,并且提供了 ...

  9. 解决ThinkPHP3.2.3框架,PDO驱动查询出来的字段名全是小写的bug

    找到文件:ThinkPHP\Library\Think\Db\Driver.class.php 找到代码: // PDO连接参数 protected $options = array( PDO::AT ...

  10. REST和SOAP区别

     转载于: http://blog.csdn.net/idafish/article/details/6308916 REST似乎在一夜间兴起了,这可能引起一些争议,反对者可以说REST是WEB诞生之 ...