iOS动画-Transform和KeyFrame动画

作者: sindri的小巢 | 来源:发表于2016-02-06 20:31 被阅读8232次

    序言

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

    作为动画篇的第二篇文章,我在从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),
          CGAffineTransformTranslate(0, -offset)
        );
        label1.alpha = 0;
        [UIView animateWithDuration: 3. animations: ^ {
            ///  还原label1的变换状态并形变和偏移label2
            label1.transform = CGAffineTransformIdentifier;
            label1.transform = CGAffineTransformConcat(
              CGAffineTransformMakeScale(0, 0),
              CGAffineTransformTranslate(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动画初体验

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

    相关文章

      网友评论

      • FengxinLi:请问下楼主 relativeDuration 这个值是什么意思呢?
        FengxinLi:@sindri的小巢 好的 谢谢了:smile:
        sindri的小巢:@FengxinLi 相对时间,就是本段动画时长占据总动画时长的百分比
      • Maj_sunshine:大神 我就看你的文章学习动画 会不会有瑕疵
        sindri的小巢:@71b03c7180a1 回头把名字发给你,纯英版的
        Maj_sunshine:@sindri的小巢 啥书 推荐一个 就是找不到书啊 才选择网上学习。看文档又太累
        sindri的小巢:@71b03c7180a1 要看书,关看博客个人感觉不够
      • 0x00chen:精神洗礼
      • 健健锅:树叶落下的 可以用 贝塞尔曲线 代码简单 而且效果很不错
        sindri的小巢:@健健锅 修改scrollView的bounds属性,用easeInOut效果
        健健锅:@Sindri的小巢 请教一个问题的,UIscrollview setcontentoffest : annimation : 利用这个函数是,如果我想修改动画的速度,比如让动画先快后慢。。。该怎么搞,大概思路
        sindri的小巢:@健健锅 贝塞尔曲线属于图层动画系列内容
      • ryugaku:马克 看起来不错
      • 1de21da9e7fc:求问大神 Transform动画 改成从下往上翻转 应该怎么更改代码 小白求指教~
        sindri的小巢:@1de21da9e7fc 使用transform实现由上往下翻转要用CATransform3D属性,这个是layer的,以后在layer动画我会介绍
      • 4bc347552fb5:CGAffineTransformMakeScale 从(0,0)到(0,1)不是显示不出来吗...不应该是CGAffineTransformMakeScale 从(1,0)到(1,1)吗..
        sindri的小巢::joy::joy:写错啦
      • 黑人的嘿:以领会精神 受教
      • 半岛夏天:__block可以去掉的
        sindri的小巢:@半岛夏天 修改代码的时候忘记去掉了。
      • Yaanco:谢谢大神分享,
      • lizhi_boy:受教了。

      本文标题:iOS动画-Transform和KeyFrame动画

      本文链接:https://www.haomeiwen.com/subject/hmzskttx.html