CSS 动画一站式指南


CSS 动画一站式指南

目录
  • 在线源码和演示

    2)0.5px 边框

    在移动端由于屏幕的分辨率较高,因此 1px 边框看起来会有点粗,那么可以直接声明 0.5px 的边框吗?可以,但是即便声明成功了,有些浏览器还是会按照 1px 来渲染。为了实现 0.5px 的边框,我们可以利用 CSS 中的 transform 来实现。

    undefined

    步骤:

    • 声明一个伪元素,令其宽高为 200%,border 宽度为 1px

    • 通过 transform: scale(.5) 将该伪元素缩小为原来的 0.5 倍。

    在线源码和演示

    1.2 过渡

    过渡的出现让状态间的切换更加丝滑,先简单介绍一下与过渡相关的属性,相关的动画实践稍候奉上。

    1.2.1 过渡属性介绍

    1)transition-property:属性

    • all :全部属性过渡( 默认 )

    • none :无属性过渡

    • String :某个属性过渡

    2)transition-duration:时间

    • Time :秒或毫秒(默认 0 )

    3)transition-timing-function:缓动函数

    • ease :逐渐变慢,等同于 cubic-bezier(.25,.1,.25,1) ( 默认 )

    • linear :匀速,等同于 cubic-bezier(0,0,1,1)

    • ease-in :加速,等同于 cubic-bezier(.42,0,1,1)

    • ease-out :减速,等同于 cubic-bezier(0,0,.58,1)

    • ease-in-out :先加速后减速,等同于 cubic-bezier(.42,0,.58,1)

    • cubic-bezier :贝塞尔曲线, (x1,y1,x2,y2) 四个值指定于曲线上的点 P1P2 ,所有值需在 [0,1] 区域内

    4)transition-delay:时延

    • Time :秒或毫秒(默认 0 )

    由于 durationdelay 的取值都是时间,所以可能会发生混淆。

    • durationdelay 作用于所有节点,包括自身的 ::before::after

    • transition 中出现两个时间值时,第一个解析为 duration ,第二个解析为 delay

    • transition 中出现一个时间值时,解析为 duration

    变换属性中一个比较重要的属性是 transition-timing-function ,它决定了过渡时间内速度是如何变化的。它的值实际上是一个贝塞尔曲线,推荐一个设置贝塞尔曲线的网站,可以根据需要设计出符合需求的贝塞尔曲线。

    1.2.2 过渡动画实践

    1)IOS 设置页面中的开关按钮

    下面这个开关在 IOS 手机的设置面板中太常见了,开关的动画效果利用 transition 这个属性也可以很轻松地实现,而且这里利用上面讲到的设置贝塞尔曲线的网站调试出了一种贝塞尔曲线使得开关按钮被打开的过程有一个刹车的效果。

    undefined

    在线源码和演示

    1.3 动画

    CSS 可以通过设置多个点精确地控制一个或一组动画,用来实现复杂的动画效果。动画由多个点组成,每个点拥有独立的状态,这些状态通过浏览器处理成过渡效果,点与点之间的过渡效果串联起来就是一个完整的动画。

    1.3.1 Animation 属性介绍

    1)animation-name:名称

    • none :无动画( 默认 )

    • String :动画名称

    2)animation-duration:时间

    • Time :秒或毫秒(默认 0 )

    3)animation-timing-function:缓动函数

    • ease :逐渐变慢,等同于 cubic-bezier(.25,.1,.25,1) ( 默认 )

    • linear :匀速,等同于 cubic-bezier(0,0,1,1)

    • ease-in :加速,等同于 cubic-bezier(.42,0,1,1)

    • ease-out :减速,等同于 cubic-bezier(0,0,.58,1)

    • ease-in-out :先加速后减速,等同于 cubic-bezier(.42,0,.58,1)

    • cubic-bezier :贝塞尔曲线, (x1,y1,x2,y2) 四个值指定于曲线上的点 P1P2 ,所有值需在 [0,1] 区域内

    • steps([,[start|end]]?) :把动画平均划分成 n等分 ,直到平均走完该动画

    • step-start :等同于 steps(1,start) ,把动画分成一步,动画执行时以左侧端点 0% 为开始

    • step-end :等同于 steps(1,end) ,把动画分成一步,动画执行时以右侧端点 100% 为开始

    4)animation-delay:时延

    • Time :秒或毫秒(默认 0 )

    5)animation-iteration-count:播放次数

    • Number :数值(默认 1 )

    • infinite :无限次

    6)animation-direction:轮流反向播放(播放次数为一次则该属性无效果)

    • normal :正常播放( 默认 )

    • alternate :轮流反向播放,奇数次数正常播放,偶数次数反向播放

    7)animation-play-state:播放状态

    • running :正在播放( 默认 )

    • paused :暂停播放

    8)animation-fill-mode:播放前后其效果是否可见

    • none :不改变默认行为( 默认 )

    • backwards :在时延所指定时间内或在动画开始前应用开始属性( 在第一个关键帧中定义 )

    • forwards :在动画结束后保持最后一个属性( 在最后一个关键帧中定义 )

    • both :向前和向后填充模式都被应用

    CSS 中动画分为两种,关键帧动画和逐帧动画。关键帧动画是将人为定义好的每一帧的状态串联成一个动画,需要通过 animation@keyframes 声明。逐帧动画的声明较为简单,使用一张逐帧长图然后配合 animation-timing-function: steps() 来完成。

    逐帧动画的声明步骤一般如下:

    • 准备一张 逐帧长图 ,该图像包含动画效果的每一帧且每帧宽高必须一致

    • steps() 里声明逐帧长图及其展示方式

    • 在指定节点中声明 animation 调用动画

    关键帧动画的声明步骤一般如下:

    • @keyframes 里声明动画名称和动画每个关键帧的状态

    • 在指定节点中声明 animation 调用动画

    关键帧动画声明示例:

    @keyframes animation-name {
        from {}
        to {}
    }
    
    /* 或 */
    @keyframes animation-name {
        p1 {}
        p2 {}
        p3 {}
    }
    

    关键帧的取值必须是 fromtoPercentagefrom 可用 0% 代替, to 可用 100% 代替,若开始或结束的关键帧无对应的状态,可不用声明 fromto0%% 不能省略,否则关键帧解析会失败。通过 animation-fill-mode 属性设置动画结束后的样式。

    1.3.2 Animation 动画实践

    1)自动打字器

    很多在线编辑器网站都有一些自动打字的效果,例如CodePen。很多同学都以为是JS实现的效果,其实查看 Chrome Devtools 发现是纯CSS实现的。观察多几次自动打字器,可发现其存在以下特点。

    • 字体都是等宽字体,等宽字体可保证每次打字时光标的移动距离都是一致的

    • 打字器的宽度由最初的 0px 逐渐增加内容后变成最终固定字数的宽度,宽度以等宽字体的个数为准

    • 光标随着每打一个字就闪烁一次,打字速度均匀,打字完毕再次重复打字

    • 整个打字过程存在两个动画,一个是打字器自增宽度,一个是光标闪烁

    • 整个打字过程一闪一闪地完成,根据其断断续续的特点可判断该动画为逐帧动画

    这里用 CSS 简单实现了一个自动打字器,在线源码和演示。

    2. CSS 动画综合实践(太空舱)

    2.1 太空舱动画介绍

    先简单介绍一下这个动画的一些背景:

    • 用户点击中间的抽奖按钮后会展示抽奖动画,然后展示将要前进的步数,小人会在跑道上跑动到相应的位置,到达此次终点后,小人展示停止动画并且显示中奖结果。

    • 用户还可以通过长按中奖按钮触发显示五连抽和十连抽,当点击五连抽时,首先会展示抽奖动画,然后展示将要前进的步数,小人会在跑道上瞬间移动到相应的位置,然后展示停止的动画,紧接着展示中奖动画。十连抽的过程和五连抽相同。

    2.2 太空舱动画实现流程

    整体流程

    大体思路是将各个功能封装为类,并且控制每个类实现的功能较小,这样可以更大程度地实现类的复用。像下面介绍的 Animate CSS 这个动画库也是封装了很多类,想要什么效果直接引用即可,这里也是同理。并在此基础上定义了一些函数对跑动等功能进行了封装,整体流程如下图所示。

    undefined

    结合上图对太空舱动画中的一些点做一些简单的说明。

    跑动的实现

    通过关键帧动画,将各个方向相邻两格之间的跑动封装为一个类。每一帧代表跑动的姿态,并且每一帧通过 translate 向前进的方向移动,最后一帧刚好到达下一格。

    以向南方向的跑动为例。

    • 向南奔跑的类

    @translateSouthX@translateSouthY 代表向南每两格 X 和 Y 方向的距离, .run-south-animation 是一个 mixin ,代表向南跑动的动画。

    @import "./animation.less";
    
    @translateSouthX: -25px;
    @translateSouthY: 13.5px;
    
    // 向南方向跑动
    .run-south {
      background: url("...");
      background-size: 1680px 255px;
      .run-south-animation(@translateSouthX, @translateSouthY);
    }
    
    • 向南奔跑的动画

    .run-south-animation 这个 mixin 是向南奔跑动画的实现,注意 animation-timing-function

    的值要设置为 steps(1) ,因为这里每两帧之间不需要补间动画。 forwards 表示最后的状态保持为动画结束时的状态。 animation-iteration-count 的值为 1 表示动画只执行一次不需要重复,因为这里是为了封装一个移动一格的动画,每次使用这个封装好的类时只应该跑动一格。

    关键帧 runSouth 中,经过多次测试最终选择了使用 28 帧,因为在此帧数下,动画较为流畅,图片的大小也可以接受。在关键帧动画中,帧数的选取至关重要,要根据具体的需求反复测试,太多或太少都不合适。

    @animationDuration: .7s
    
    // 向南奔跑动画
    .run-south-animation(@translateX, @translateY) {
      animation: runSouth @animationDuration forwards;
      animation-iteration-count: 1;
      animation-timing-function: steps(1);
    
      @keyframes runSouth {
        0% {
          transform: translate(0, 0);
          background-position: 0px -2*(@singleAstronautWidth+@verticalInterval);
        }
    
    ...   
    
        50.4% {
          transform: translate(@translateX / 28 * 14, @translateY / 28 * 14);
          background-position: -840px -2*(@singleAstronautWidth+@verticalInterval);
        }
    
    ...
    
        100% {
          transform: translate(@translateX / 28 * 28, @translateY / 28 * 28);
          background-position: -1680px -2*(@singleAstronautWidth+@verticalInterval);
        }
      }
    }
    

    如何再次触发跑动动画

    当第一次将跑动的类(假设向南)赋予展示小人的元素时小人可以正常跑动,但是如果紧接着再将这个类赋予这个元素时,即使组件中的 state 改变了,也不会触发小人继续向南跑动了。

    为了验证这一点,写了一个小 demo,如下代码所示,在 3 s 后改变 state 中的 cls 和 doc,由于 cls 没有发生变化,即使 doc 发生了变化,也不会触发 first 类中的动画重新执行,在线源码和演示。

    class App extends React.Component {
      
      state = {
        cls: 'first',
        doc: 'hello hello'
      }
      
      componentDidMount() {
        setTimeout(() => {
          this.setState({
            cls: 'first',
            doc: 'world world'
          })
        }, 3000)
      }
    
      render() {
        return 

    {this.state.doc}

    ; } } ReactDOM.render(, document.getElementById("root"));
    body {
      height: 100vh;
      margin: 0;
      display: grid;
      place-items: center;
      max-width: 250px;
      margin: 0 auto;
    }
    
    .first {
      animation: myfirst 2s linear both;
      animation-iteration-count: 1;
    }
    
    @keyframes myfirst
    {
        from {background: red;}
        to {background: yellow;}
    }
    

    当 state 中的 doc 发生变化时,h1 元素只会更新变化的属性,并不会将整个元素重新渲染。由 react 中的 diff 算法可知,要想让 h1 元素重新渲染,可以给它赋予一个 key 值,然后在 3s 后改变这个 key 值,此时两次 key 值不同就会触发 h1 元素整个重新渲染,然后就可以让动画再次执行。

    因此在 state 中新增一个 key 值,并将其赋予 h1 元素的 key 属性,在 3s 后改变这个 key 值就可以使动画重新执行。具体的代码可以参考:在线源码和演示。

    移动航天小人到指定位置的实现(moveToSpecialPosByCurPos)

    • 获取航天小人将要到的指定位置

    • 根据将要到的指定位置获取下一步的前进方向

    • 改变展示小人元素的位置(使用 transform 属性)

    监听动画结束事件,来对下一步的行为做出判断

    CSS 动画事件

    • animationstart 某个 CSS 动画开始时触发。

    • animationend 某个 CSS 动画完成时触发。

    • animationiteration 某个 CSS 动画完成后重新开始时触发。

    前面我们封装的各个方向的类,每调用一次,小人会向前跑一步,跑完一步后会触发 animationend 事件,在 animationend 事件的回调函数中处理下面应该如何走。

    • 判断是否是连抽,如果是连抽走连抽逻辑

    • 如果不是连抽,将剩余的步数减 1,当前的步数加 1

    • 判断当前的位置是否大于赛道的总步数,如果大于赛道总步数,将当前位置设置为起点

    • 如果小人当前剩余的步数为0,那么改变ELotteryStatus的状态触发中奖动画的展示,并且触发小人停止时的发光动画,在1s后隐藏小人的发光动画。

    • 如果小人当前的剩余步数不为0,根据当前的位置获取下一步的前进方向,然后将相应前进方向跑动动画的类赋予给展示小人的元素

    • 改变展示小人元素的 key 值触发再次执行动画,小人继续向前跑动。

    连抽时瞬间移动的实现

    • 当前位置加上抽奖的步数得到最终的位置

    • 最终的位置是否超过赛道长度,如果超过了赛道长度,利用最终的位置减去赛道的总长度

    • 调用 moveToSpecialPosByCurPos 函数移动到指定位置

    • 触发小人停止时的发光动画,并且在 1s 后隐藏小人的发光动画,改变 ELotteryStatus 状态展示中动画

    • 从后台拉取最新的位置,校正小人的位置

    3. CSS 动画库

    在了解 CSS 动画的原理之后,实际做动画需求的时候,一种比较推荐的做法是站在巨人的肩膀上。业界已经有很多优秀的开源动画库了,可以看一下这些开源的动画库是否可以用得上。

    这里主要介绍两个比较常用的 CSS 动画库 Animista Animate CSS

    Animista 提供了很多动画类型,并且还提供了很多参数供你设置,设置完成之后就可以获得该动画的 CSS 代码了。

    Animate CSS 则是提供了很多动画类,通过给元素设置不同的类就可以实现不同的动画效果。

    3.1 Animista

    Animista 是一个在线动画生成器,同时也是一个动画库,基本使用步骤如下:

    1)选择动画,调节参数

    首先选择你想要的动画类型,然后根据你的需求对该动画的参数进行调节。

    undefined

    2)生成CSS代码

    设置完满足自己需求的动画后,我们可以直接从网站上获取代码,甚者还可以进行压缩。

    undefined

    3.2 Animate CSS

    Animate CSS 是最著名的动画库之一,接下来介绍一下 Animate CSS 的一些基本用法,详细的用法可以去官网查询。

    基本用法

    首先在需要添加动画的元素上添加类 animated ,然后是动画的名字。

    Animate CSS ****还提供了一些类来控制动画的一些属性,例如动画的延迟和速度等。

    • delay

      通过添加 delay 类来延迟动画的播放。

    • speed

    通过添加这些类(slow|slower|fast|faster)之一来控制动画速度。

    这里用 Animate CSS 库写了一个小的 demo ,在线源码和演示。

    4. CSS 动画性能优化

    回流又叫重排,指几何属性需要改变的渲染,例如当元素的尺寸,布局等发生变化时一般会引发回流。重绘,指外观属性需要改变的渲染,例如当元素的背景色发生变化时一般会引发重绘。

    一个 CSS 动画往往会涉及尺寸,位置,颜色等属性的变化,如果处理不当就会引发不断地回流和重绘,导致页面卡顿,尤其在性能有限的移动端这种问题尤为严重。

    在上面提到,几何属性改变时一般会引发回流,外观属性改变时一般会引发重绘,那么在 CSS 中哪些属性是几何属性,哪些属性是外观属性呢?这里简单地总结了一下。

    几何属性:包括布局、尺寸等可用数学几何衡量的属性。

    • 布局: displayfloatpositionlisttableflexcolumnsgrid

    • 尺寸: marginpaddingborderwidthheight

    外观属性:包括界面、文字等可用状态向量描述的属性

    • 界面: appearanceoutlinebackgroundmaskbox-shadowbox-reflectfilteropacityclip

    • 文字: textfontword

    我们知道回流一定会引发重绘,重绘不一定会引发回流。回流成本比重绘成本高得多,一个节点的回流很有可能导致子节点、兄弟节点或祖先节点的回流。频繁触发回流会使得页面不断渲染,从而引发严重的性能问题,因此我们一定要尽可能地避免回流,减少重绘。

    接下来介绍一些 CSS 动画性能优化的方法。

    4.1 使用 visibility:hidden 替换 display:none

    从以下四个方面对比一下 display:nonevisibility:hidden ,方便书写, display:none 简称 DNvisibility:hidden 简称 VH

    • 占位表现

    • DN不占据空间

    • VH占据空间

    • 触发影响

    • DN触发回流重绘

    • VH触发重绘

    • 过渡影响

    • DN影响过渡不影响动画

    • VH不影响过渡不影响动画

    • 株连效果

    • DN后自身及其子节点全都不可见

    • VH后自身及其子节点全都不可见但可声明子节点 visibility:visible 单独显示

    两者的 占位表现触发影响株连效果 就能说明 VH 代替 DN 的好处,如果两者都能实现需求的情况下推荐使用 visibility:hidden

    4.2 使用 transform 代替 top

    top 是几何属性,操作 top 会改变节点位置从而引发回流,使用 transform:translate3d(x,0,0) 代替 top ,只会引发图层重绘,还会间接启动GPU加速,因此更加推荐使用 transform 来代替 top。

    4.3 避免使用 Table 布局

    当然 Table 布局现在已经很少用了,不过在这里还是提一下,来指出它所带来的问题,避免引入类似的问题。牵一发而动全身用在 Table 布局身上再合适不过了,可能很小的一个改动就会造成整个

    回流,大家如果感兴趣可以用 Chrome DevtoolsPerformance 调试看看。对于类似 table 布局的结构,建议用
    • 等标签进行取代。

      4.4 避免规则层级过多

      浏览器的CSS解析器在解析css文件时,对CSS规则是从右到左匹配查找的,样式层级过多会影响回流重绘效率,建议保持CSS规则在 3层 左右。

      4.5 避免节点属性值放在循环里当成循环变量

      像下面这段代码就存在很大的问题,每次循环操作 DOM 都会发生回流,应该在循环外使用变量保存一些不会变化的DOM 映射值。

      for (let i = 0; i < 10000; i++) {
      const top = document.getElementById("css").style.top;
          console.log(top);
      }
      

      建议修改为下面这样:

      const top = document.getElementById("css").style.top;
      for (let i = 0; i < 10000; i++) {
          console.log(top);
      }
      

      4.6 将频繁回流重绘的节点设置为图层

      在浏览器中设置频繁回流或重绘的节点为一张新图层,因为新图层能够阻止节点的渲染行为影响别的节点,这张图层里如何变化都无法影响到其他图层。

      设置新图层通常有以下两种方式:

      • 将节点设置为