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实现你的动画之前,我先介绍几个常用的函数:

/// 用来连接两个变换效果并返回。返回的t = t1 * t2
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2)

/// 矩阵初始值。[ 1 0 0 1 0 0 ]
CGAffineTransformIdentity

/// 自定义矩阵变换,需要掌握矩阵变换的知识才知道怎么用。参照上面推荐的原理链接
CGAffineTransformMake(CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)

/// 旋转视图。传入参数为 角度 * (M_PI / 180)。等同于 CGAffineTransformRotate(self.transform, angle)
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)

/// 缩放视图。等同于CGAffineTransformScale(self.transform, sx, sy)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)

/// 缩放视图。等同于CGAffineTransformTranslate(self.transform, tx, ty)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

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

文字变形过程

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

- (void)viewDidAppear: (BOOL)animated {
    label1.transform = CGAffineTransformMakeScale(1, 0);
    label1.alpha = 0;
    [UIView animateWithDuration: 3. animations: ^ {
        label1.transform = CGAffineTransformMakeScale(1, 1);
        label2.transform = CGAffineTransformMakeScale(1, 0.1);
        label1.alpha = 1;
        label2.alpha = 0;
    }];
}

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

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

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

- (void)viewDidAppear: (BOOL)animated {
    ///  初始化动画开始前label的位置
    CGFloat offset = label1.frame.size.height * 0.5;
    
    label1.transform = CGAffineTransformConcat(
      CGAffineTransformMakeScale(0, 0),
      CGAffineTransformMakeTranslation(0, -offset)
    );
    label1.alpha = 0;
    [UIView animateWithDuration: 3. animations: ^ {
        ///  还原label1的变换状态并形变和偏移label2
        label1.transform = CGAffineTransformIdentifier;
        label2.transform = CGAffineTransformConcat(
          CGAffineTransformMakeScale(0, 0),
          CGAffineTransformMakeTranslation(0, offset)
        );
        label1.alpha = 1;
        label2.alpha = 0;
    }];
}

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

keyframe动画

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

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

1.png

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

[self moveLeafWithOffset: (CGPoint){ 15, 80 } completion: ^(BOOL finished) {
    [self moveLeafWithOffset: (CGPoint){ 30, 105 } completion: ^(BOOL finished) {
        [self moveLeafWithOffset: (CGPoint){ 40, 110 } completion: ^(BOOL finished) {
            [self moveLeafWithOffset: (CGPoint){ 90, 80 } completion: ^(BOOL finished) {
                [self moveLeafWithOffset: (CGPoint){ 80, 60 } completion: nil duration: 0.6];
            } duration: 1.2];
        } duration: 1.2];
    } duration: 0.6];
} duration: 0.4];

- (void)moveLeafWithOffset: (CGPoint)offset completion: (void(^)(BOOL finished))completion duration: (NSTimeInterval)duration
{
    [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
        CGPoint center = _leaf.center;
        center.x += offset.x;
        center.y += offset.y;
        _leaf.center = center;
    } completion: completion];
}

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

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

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

对于这两个问题,UIView也提供了另一种动画方式来帮助我们解决这两个问题 —— keyframe动画:
+ (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations
第一个方法是创建一个关键帧动画,第二个方法用于在动画的代码块中插入关键帧动画信息,两个参数的意义表示如下:

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

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

添加关键帧方法参数说明

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

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

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

[UIView animateKeyframesWithDuration: 4 delay: 0 options: UIViewKeyframeAnimationOptionCalculationModeLinear animations: ^{
    __block CGPoint center = _leaf.center;
    [UIView addKeyframeWithRelativeStartTime: 0 relativeDuration: 0.1 animations: ^{
        _leaf.center = (CGPoint){ center.x + 15, center.y + 80 };
    }];
    [UIView addKeyframeWithRelativeStartTime: 0.1 relativeDuration: 0.15 animations: ^{
        _leaf.center = (CGPoint){ center.x + 45, center.y + 185 };
    }];
    [UIView addKeyframeWithRelativeStartTime: 0.25 relativeDuration: 0.3 animations: ^{
        _leaf.center = (CGPoint){ center.x + 90, center.y + 295 };
    }];
    [UIView addKeyframeWithRelativeStartTime: 0.55 relativeDuration: 0.3 animations: ^{
        _leaf.center = (CGPoint){ center.x + 180, center.y + 375 };
    }];
    [UIView addKeyframeWithRelativeStartTime: 0.85 relativeDuration: 0.15 animations: ^{
        _leaf.center = (CGPoint){ center.x + 260, center.y + 435 };
    }];
    [UIView addKeyframeWithRelativeStartTime: 0 relativeDuration: 1 animations: ^{
        _leaf.transform = CGAffineTransformMakeRotation(M_PI);
    }];
} completion: nil];

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

尾言

本文作为动画篇的第二篇博客,到了这里UIView的所有动画教程已经完成,在之后的文章中将进一步讲解autolayout动画和图层层次的动画。时值新年,祝愿各位🐵年快乐,心想事成!本文demo地址

上一篇:从UIView动画说起
下一篇:layout动画初体验

转载请注明地址和原文作者

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 序言 追求美好是人的天性,这是猿们无法避免的。我们总是追求更为酷炫的实现,如果足够仔细,我们不难发现一个好的动画通...
    姚钦阅读 708评论 0 3
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 4,999评论 5 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,321评论 6 30
  • 前言: UIVIew Animation 是 iOS 提供的最基础的一组用于实现 UIView 动画的类库。在 U...
    谢谢生活阅读 3,190评论 0 5
  • iOS核心动画-Core Animation 概论 目标:1. 学会使用图层精简非交互式绘图;2. 通过核心动画创...
    ValienZh阅读 1,838评论 2 9