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,看名字知道,之前的一篇学习笔 ...
随机推荐
- ES6的特性(译+注解)
介绍 ES6,也叫ECMAScript2015(以下统称ES6),是ECMAScript标准的最新版本.这个标准在2015年6月份被正式批准.ES6是js语言很有意义的一次更新,也是2009年ES5被 ...
- Asp.Net MVC在过滤器中使用模型绑定
废话不多话,直接上代码 1.创建MVC项目,新建一个过滤器类以及使用到的实体类: public class DemoFiltersAttribute : AuthorizeAttribute { pu ...
- HDU 1013 Digital Roots(字符串,大数,九余数定理)
Digital Roots Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Tot ...
- HDU 2549 壮志难酬(字符串,处理小数点)
/* 给你一个小数x,让你算出小数点后第n位是什么,(1 <= n <= 6) Input 首先输入一个t,表示有t组数据,跟着t行: 每行输入一个小数(输入数据保证一定是a.b的形式,为 ...
- CodeForces 32C Flea
题目链接:http://codeforces.com/problemset/problem/32/C 本文链接:http://www.cnblogs.com/Ash-ly/p/5513436.html ...
- 《Machine Learning》(第一章)序章
关键词:机器学习,基本术语,假设空间,归纳偏好,机器学习用途 一.机器学习概述 机器学习是一门从数据中,经过计算得到模型(Model)的一种过程,得到的模型不仅能反应出训练数据集中所蕴含的规律,并且能 ...
- hdu6059( Trie )
hdu6059 题意 给定数组 \(A\) ,问有多少对下标 \((i, j, k)\) 满足 \(i < j < k\) 且 \((A[i] \ xor \ A[j]) < (A[ ...
- RabbitMQ (一) 简介和基本概念
原文:https://blog.csdn.net/vbirdbest/article/details/78577043 一.简介 MQ全称为Message Queue, 消息队列(MQ)是一种应用程序 ...
- Coloring Dominoes
问题 E: Coloring Dominoes 时间限制: 1 Sec 内存限制: 128 MB提交: 279 解决: 95[提交] [状态] [讨论版] [命题人:] 题目描述 We have ...
- leetcode122 Best Time to Buy and Sell Stock
题意:有一个数组,第i个数据代表的是第i天股票的价格,每天只能先卖出再买进(可以不卖出也可以不买进),求最大收益. 思路:自己去弄几个数组比划比划就知道了,比如[1,2,5,3,6],第一天买进,第二 ...