iOS 开发之动画篇 - Transform和KeyFrame动画

序言

追求美好是人的天性,这是猿们无法避免的。我们总是追求更为酷炫的实现,如果足够仔细,我们不难发现一个好的动画通过步骤分解后本质上不过是一个个简单的动画实现,正是这些基本的动画在经过合理的搭配组合后化腐朽为神奇,令人惊艳。因此,掌握最基本的动画是完成酷炫开发之旅的根本。

作为动画篇的第二篇文章,我在从UIView动画说起简单介绍了关于UIView的几种基本动画,这几种动画的搭配让我们的登录界面富有灵性生动,但是这几种动画总是无法满足我们对于动画的需求。同样的,本文将从一个小demo开始讲解强大的transform动画以及关键帧keyFrame动画。

demo动效图

可以看到两个动画:叶子被风吹落以及左边的文字从summer变化到autumn,这两个动画都是基于强大的transform形变,其中叶子的飘落动画通过关键帧动画实现。demo链接

transform动画

transform是一个非常重要的属性,它在矩阵变换的层面上改变视图的显示效果,完成旋转、形变、平移等等操作。在它被修改的同时,视图的frame也会被真实改变。有两个数据类型用来表示transform,分别是CGAffineTransformCATransform3D。前者作用于UIView,后者为layer层次的变换类型。基于后者可以实现更加强大的功能,但我们需要先掌握CGAffineTransform类型的使用。同时,本文讲解也是这个变换类型。

对于想要了解矩阵变换是如何作用实现的,可以参考这篇博客:CGAffineTransform 放射变换

talk is cheap show you the code

在开始使用transform实现你的动画之前,我先介绍几个常用的函数:

[js] view
plain
 copy

  1. /// 用来连接两个变换效果并返回。返回的t = t1 * t2
  2. CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2)
  3. /// 矩阵初始值。[ 1 0 0 1 0 0 ]
  4. CGAffineTransformIdentity
  5. /// 自定义矩阵变换,需要掌握矩阵变换的知识才知道怎么用。参照上面推荐的原理链接
  6. CGAffineTransformMake(CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)
  7. /// 旋转视图。传入参数为 角度 * (M_PI / 180)。等同于 CGAffineTransformRotate(self.transform, angle)
  8. CGAffineTransformMakeRotation(CGFloat angle)
  9. CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
  10. /// 缩放视图。等同于CGAffineTransformScale(self.transform, sx, sy)
  11. CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
  12. CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
  13. /// 缩放视图。等同于CGAffineTransformTranslate(self.transform, tx, ty)
  14. CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
  15. CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

我把demo左下角文字的变形过程记录下来。这里推荐mac上面的一款截取动图的程序licecap,非常简单好用。博主用它来分解动画步骤,然后进行重现。

文字变形过程

不难看出在文字的动画中做了两个处理:y轴上的形变缩小、透明度的渐变过程。首先在项目中新增两个UILabel,分别命名为label1、label2.然后在viewDidAppear中加入这么一段代码:

[js] view
plain
 copy

  1. - (void)viewDidAppear: (BOOL)animated {
  2. label1.transform = CGAffineTransformMakeScale(0, 0);
  3. label1.alpha = 0;
  4. [UIView animateWithDuration: 3. animations: ^ {
  5. label1.transform = CGAffineTransformMakeScale(0, 1);
  6. label2.transform = CGAffineTransformMakeScale(0, 0.1);
  7. label1.alpha = 1;
  8. label2.alpha = 0;
  9. }];
  10. }

这里解释一下为什么label2为什么在动画中y轴逐渐缩小为0.1而不是0。如果我们设为0的话,那么在动画提交之后,label2会直接保持动画结束的状态(这是出于性能优化自动完成的),因此在使用任何缩小的形变时,你可以将缩小值设置的很小,只要不是0。

运行你的代码,文字的形变过程你已经做出来了,但是demo中的动画不仅仅是形变,还包括位移的过程。很显然,我们可以通过改变center的位置来实现这个效果,但这显然不是我们今天想要的结果,实现新的动画方式来实现更有意义。

动画开始时形变出现的label高度为0,然后逐渐的的变高变为height,而label从头到尾基于顶部的位置不发生改变。因此动画开始前这个label在y轴上的位置是0,在完成显示之后的y轴中心点为height / 2(基于label自身的坐标系而言),那么动画的代码就可以写成这样:

[js] view
plain
 copy

  1. - (void)viewDidAppear: (BOOL)animated {
  2. ///  初始化动画开始前label的位置
  3. CGFloat offset = label1.frame.size.height * 0.5;
  4. label1.transform = CGAffineTransformConcat(
  5. CGAffineTransformMakeScale(0, 0),
  6. CGAffineTransformTranslate(0, -offset)
  7. );
  8. label1.alpha = 0;
  9. [UIView animateWithDuration: 3. animations: ^ {
  10. ///  还原label1的变换状态并形变和偏移label2
  11. label1.transform = CGAffineTransformIdentifier;
  12. label1.transform = CGAffineTransformConcat(
  13. CGAffineTransformMakeScale(0, 0),
  14. CGAffineTransformTranslate(0, offset)
  15. );
  16. label1.alpha = 1;
  17. label2.alpha = 0;
  18. }];
  19. }

调整两个label的位置,并且设置其中一个透明显示。然后运行这段代码,你会发现文字转变过程的动画完成了。

keyframe动画

将文章开头的gif图另存为到本地,然后使用预览打开看看,你会发现预览中的gif图变成了很多张的图片。实际上,无论是动画、电影、CG等动态效果,都可以看做是一张张图片接连渲染实现的,而这些图片切换的速度足够快时我们就会当做是动画。在此之前我们所讲述的平移视图在UIView动画提交之后系统会根据动画时长计算出视图移动的所有帧界面,然后逐个渲染。

回到我们demo中的落叶动画来,我总共对叶子的center进行过五次修改,我将落叶平移的线性路径绘制出来并且标注关键的转折点:

1.png

上面这个平移用UIView动画代码要如何实现呢?毫无疑问,我们需要不断的嵌套UIView动画的使用来实现,具体代码如下:

[js] view
plain
 copy

  1. [self moveLeafWithOffset: (CGPoint){ 15, 80 } completion: ^(BOOL finished) {
  2. [self moveLeafWithOffset: (CGPoint){ 30, 105 } completion: ^(BOOL finished) {
  3. [self moveLeafWithOffset: (CGPoint){ 40, 110 } completion: ^(BOOL finished) {
  4. [self moveLeafWithOffset: (CGPoint){ 90, 80 } completion: ^(BOOL finished) {
  5. [self moveLeafWithOffset: (CGPoint){ 80, 60 } completion: nil duration: 0.6];
  6. } duration: 1.2];
  7. } duration: 1.2];
  8. } duration: 0.6];
  9. } duration: 0.4];
  10. - (void)moveLeafWithOffset: (CGPoint)offset completion: (void(^)(BOOL finished))completion duration: (NSTimeInterval)duration
  11. {
  12. [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
  13. CGPoint center = _leaf.center;
  14. center.x += offset.x;
  15. center.y += offset.y;
  16. _leaf.center = center;
  17. } completion: completion];
  18. }

看起来还蛮容易的,上面的代码只是移动叶子,在gif图中我们的叶子还有旋转,因此我们还需要加上这么一段代码:

[UIView animateWithDuration: 4 animations: ^{
    _leaf.transform = CGAffineTransformMakeRotation(M_PI);
}];

那么ok,运行这段代码看看,落叶的移动非常的生硬,我们可以明显的看到拐角。其次,这段代码中的duration传入是没有任何意义的(传入一个固定的动画时长无法体现出在落叶飘下这一过程中的层次步骤)

对于这两个问题,UIView也提供了另一种动画方式来帮助我们解决这两个问题 —— keyframe动画:

[js] view
plain
 copy

  1. + (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion
  2. + (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations

第一个方法是创建一个关键帧动画,第二个方法用于在动画的代码块中插入关键帧动画信息,两个参数的意义表示如下:

  • frameStartTime  表示关键帧动画开始的时刻在整个动画中的百分比
  • frameDuration  表示这个关键帧动画占用整个动画时长的百分比。

我做了一张图片来表示参数含义:

添加关键帧方法参数说明

对比UIView动画跟关键帧动画,关键帧动画引入了动画占比时长的概念,这让我们能控制每个关键帧动画的占用比例而不是传入一个无意义的动画时长 —— 这让我们的代码更加难以理解。当然,除了动画占比之外,关键帧动画的options参数也让动画变得更加平滑,下面是关键帧特有的配置参数:

[js] view
plain
 copy

  1. UIViewKeyframeAnimationOptionCalculationModeLinear      // 连续运算模式,线性
  2. UIViewKeyframeAnimationOptionCalculationModeDiscrete    // 离散运算模式,只显示关键帧
  3. UIViewKeyframeAnimationOptionCalculationModePaced       // 均匀执行运算模式,线性
  4. UIViewKeyframeAnimationOptionCalculationModeCubic       // 平滑运算模式
  5. UIViewKeyframeAnimationOptionCalculationModeCubicPaced  // 平滑均匀运算模式

在demo中我使用的是UIViewKeyframeAnimationOptionCalculationModeCubic,这个参数使用了贝塞尔曲线让落叶的下落动画变得更加平滑。效果可见最开始的gif动画,你可以修改demo传入的不同参数来查看效果。接下来我们就根据新的方法把上面的UIView动画转换成关键帧动画代码,具体代码如下:

[js] view
plain
 copy

  1. [UIView animateKeyframesWithDuration: 4 delay: 0 options: UIViewKeyframeAnimationOptionCalculationModeLinear animations: ^{
  2. __block CGPoint center = _leaf.center;
  3. [UIView addKeyframeWithRelativeStartTime: 0 relativeDuration: 0.1 animations: ^{
  4. _leaf.center = (CGPoint){ center.x + 15, center.y + 80 };
  5. }];
  6. [UIView addKeyframeWithRelativeStartTime: 0.1 relativeDuration: 0.15 animations: ^{
  7. _leaf.center = (CGPoint){ center.x + 45, center.y + 185 };
  8. }];
  9. [UIView addKeyframeWithRelativeStartTime: 0.25 relativeDuration: 0.3 animations: ^{
  10. _leaf.center = (CGPoint){ center.x + 90, center.y + 295 };
  11. }];
  12. [UIView addKeyframeWithRelativeStartTime: 0.55 relativeDuration: 0.3 animations: ^{
  13. _leaf.center = (CGPoint){ center.x + 180, center.y + 375 };
  14. }];
  15. [UIView addKeyframeWithRelativeStartTime: 0.85 relativeDuration: 0.15 animations: ^{
  16. _leaf.center = (CGPoint){ center.x + 260, center.y + 435 };
  17. }];
  18. [UIView addKeyframeWithRelativeStartTime: 0 relativeDuration: 1 animations: ^{
  19. _leaf.transform = CGAffineTransformMakeRotation(M_PI);
  20. }];
  21. } completion: nil];

可以看到相比UIView的动画,关键帧动画更加直观的让我们明白每一次平移动画的时间占比,代码也相对的更加简洁。

时间: 2024-10-09 09:36:57

iOS 开发之动画篇 - Transform和KeyFrame动画的相关文章

iOS开发——实战OC篇&环境搭建之Xib(玩转UINavigationController与UITabBarController)

iOS开发——实战OC篇&环境搭建之Xib(玩转UINavigationController与UITabBarController) 前面我们介绍了StoryBoard这个新技术,和纯技术编程的代码创建界面,本篇我们将介绍一个老的技术,但是在很多的公司或者库里面还是使用这个技术,既然如此它肯定有他的好处,至于好处这里我就不一一介绍了.在Xcode5之前是只能使用Xib或者代码的,而代码又对于很多初学者来说算是一个难题.毕竟不知道怎么下手.所以我就总结了一下这段时间自己编写程序的一个实例来说明怎么

iOS开发——实战OC篇&环境搭建之纯代码(玩转UINavigationController与UITabBarController)

iOS开发——实战OC篇&环境搭建之纯代码(玩转UINavigationController与UITabBarController) 这里我们就直接上实例: 一:新建一个项目singleView Controller,命名未iCocos 二:由于我们使用的纯代码实现的,所以删除其中的StoryBoard和Viewtroller的两个文件 三:新建一个继承自TabBar Controller的类,我们命名问iCocos ViewController 三:在Appdelegate的实现文件中导入刚刚

学习IOS开发网络多线程篇--NSThread/GCD/

NSThread:利用NSThread创建和启用一个线程 1. NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];,调用后调用[thread start]; 2. 创建线程后自动启动线程 ,[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; 3. 隐式创建

IOS开发数据存储篇—IOS中的几种数据存储方式

IOS开发数据存储篇—IOS中的几种数据存储方式 发表于2016/4/5 21:02:09  421人阅读 分类: 数据存储 在项目开发当中,我们经常会对一些数据进行本地缓存处理.离线缓存的数据一般都保存在APP所在的沙盒之中.一般有以下几种: 1.PList(XML属性列表) 在使用plist进行数据存储和读取,只适用于系统自带的一些常用类型才能用,且必须先获取路径相对麻烦 //写入文件 NSString *doc = [NSSearchPathForDirectoriesInDomains(

IOS开发——UI进阶篇(十七)CALayer,核心动画基本使用

一.CALayer简介 1.CALayer在iOS中,文本输入框.一个图标等等,这些都是UIView你能看得见摸得着的东西基本上都是UIView,比如一个按钮.一个文本标签.一个其实UIView之所以能显示在屏幕上,完全是因为它内部的一个图层 在创建UIView对象时,UIView内部会自动创建一个图层(即CALayer对象),通过UIView的layer属性可以访问这个层 @property(nonatomic,readonly,retain) CALayer *layer; 当UIView需

iOS开发——淫技篇&iOS开发中各种淫技总结(五)

淫技篇&iOS开发中各种淫技总结(五) ARC的使用: ARC并不能避免所有的内存泄露.使用ARC之后,工程中可能还会有内存泄露,不过引起这些内存泄露的主要原因是:block,retain循环,对CoreFoundation对象(通常是C结构)管理不善,以及真的是代码没写好. reuseIdentifier 在iOS程序开发中一个普遍性的错误就是没有正确的为UITableViewCells.UICollectionViewCells和UITableViewHeaderFooterViews设置r

IOS开发--数据持久化篇文件存储(二)

前言:个人觉得开发人员最大的悲哀莫过于懂得使用却不明白其中的原理.在代码之前我觉得还是有必要简单阐述下相关的一些知识点. 因为文章或深或浅总有适合的人群.若有朋友发现了其中不正确的观点还望多多指出,不胜感激. 承接上篇博客我们来看看IOS开发中是如何将一个自定义的对象进行归档的 本篇博客将介绍以下几个方面的内容 1)普通的单个对象归档操作 2)拥有继承关系的对象归档 3)同时将多个对象进行归档 1.普通的单个对象归档操作 首先我们来看下最简单的单个对象归档操作 1.自定义一个跟小明一样有名的类(

iOS开发——实用技术OC篇&单例模式的实实现(ACR&MRC)

单例模式的实实现(ACR&MRC) 在iOS开发中单例模式是一种非常常见的模式,虽然我们自己实现的比较少,但是,系统却提供了不少的到来模式给我们用,比如最常见的UIApplication,Notification等, 那么这篇文章就简单介绍一下,我们开发中如果想要实现单例模式要怎么去实现! 单例模式顾名思义就是只有一个实例,它确保一个类只有一个实例,并且自行实例化并向整个系统提供这个实例.它经常用来做应用程序级别的共享资源控制.这个模式使用频率非常高,通过一个单例类,可以实现在不同窗口之间传递数

iOS开发笔记 - 网络篇

计算机网络基础 ??计算机网络是多台独立自主的计算机互联而成的系统的总称,最初建立计算机网络的目的是实现信息传递和资源共享. ??如果说计算机是第二次世界大战的产物,那么计算机网络则是美苏冷战的产物.20世纪60年代初期,美国国防部领导的ARPA提出研究一种崭新的.能够适应现代战争的.生存性很强的通信系统并藉此来应对苏联核攻击的威胁,这个决定促使了分组交换网的诞生,也奠定今天计算机网络的原型,这是计算机网络发展史上第一个里程碑式的事件. ??第二个里程碑式的事件是20世纪80年代初,国际标准化组