动画

本章概要

  • 使用关键帧动画实现页面复杂动效
  • 页面加载时播放动画的方法
  • 使用旋转动画提供反馈
  • 吸引用户关注保存按钮,提醒用户保存信息

在前两章中,我们构建了几种过渡特效,实现了将元素从一个状态平滑变切换另一个状态的效果。这为页面增添了动态效果,用户体验方面也增加了视觉趣味性,但有时候仅仅使用过渡是不够的。

过渡是直接从一个地方变换到另一个地方;相比之下,我们偶尔也希望某个元素的变化过程是迂回的路径(roundabout path);又或者,我们可能想让元素在动画结束后再回到起始位置。而过渡特效是无法满足这类需求的。为了对页面变化有更加精细的把控,CSS 提供了关键帧动画(keyframe animation)技术。

关键帧(keyframe) 是指动画过程中的某个特定时刻。人们先定义出一些关键帧,浏览器再负责填充(或插值计算)补全这些关键帧之间的帧图像(如图 17.1 所示)。这些插值产生的帧值将在关键帧之间连续变化,因此画面会从一个关键帧平滑地过渡到下一个关键帧。

【图 17.1 定义关键帧后,浏览器会对其间的所有帧图像作插值处理】

从原理上看,过渡其实和关键帧动画类似:我们定义出第一帧(起点)和最后一帧(终点),浏览器算出所有的中间值,使得元素可以在这些值之间平滑过渡。但在关键帧动画中,我们不再局限于只定义两个点,而是想加多少就加多少。浏览器负责填充一个个点与点之间的各类取值,直到最后一个关键帧,最终产生一系列无缝衔接的过渡效果。

最后这一章将介绍如何创建关键帧动画。我们会在前一章创建的示例页中添加一些动画效果,然后探索它们的一些其他使用方式。动画并非只能让页面变得生动有趣,还能向用户传达有意义的反馈。

17.1 关键帧 Keyframes
CSS 中的动画包括两部分:用于定义动画的 @keyframe 规则,以及给某个元素添加动画效果的 animation 属性(property)。

不妨实现一个简单的动画效果来熟悉一下相关语法。这个动画包含三个关键帧,如图 17.2 所示。在第一帧中,元素是红色的;第二帧中元素是浅蓝色的,并且向右移动了 100px;而在最后一帧中,元素则是淡紫色的,并且回到了左侧的初始位置。

【图 17.2 分别对元素的颜色和位置添加了动画的三个关键帧】

该动画对 background-color 和 transform 这两个属性做了一些调整,对应的关键帧规则样式如代码清单 17.1 所示。新建样式表 style.css 并添加下列代码:

代码清单 17.1 定义关键帧的@规则样式写法

@keyframes over-and-back { /* 给动画命名 */
  0% { /* 第一个关键帧声明 */
    background-color: hsl(0, 50%, 50%);
    transform: translate(0);
  }
  50% { /* 第二个关键帧出现在动画进行到一半时 */
    transform: translate(50px);
  }

  100% { /* 最后一个关键帧 */
    background-color: hsl(270, 50%, 90%);
    transform: translate(0);
  }
}

关键帧动画必须命名。本例定义了一个名为 over-and-back 的动画。该动画使用百分比又定义了三个关键帧。这些百分比分别代表每个关键帧在动画中出现的某个时刻:一个在动画的初始时刻(0%),一个在中间(50%),一个在结束时(100%)。每个关键帧代码块内的样式声明则分别定义了当前关键帧的具体样式。

在本例中,我们同时为两个属性添加了动画;但同时也要注意,我们并没有对每个关键帧都设置了这两个属性。transform 将元素从初始位置平移到右侧,然后再移回原位;而 background-color 在 50% 的关键帧中并没有设定。也就是说元素会由红色(即 0% 处)过渡到淡紫色(即 100% 处);到 50% 时,元素的背景色恰好是这两种颜色的中间值。

下面我们把这些代码添加到页面中看看实际效果。新建一个 HTML 文档,然后根据代码清单 17.2 中的内容同步更新文档。

代码清单 17.2 仅包含一个带动画效果盒子的示例页 HTML 标记

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <div class="box"></div><!-- 待添加动画的元素 -->
  </body>
</html>

接下来,在样式表中给 box 元素设计样式并添加动画特效。复制以下代码即可。

代码清单 17.3 给元素添加动画特效的示例样式代码

.box {
  width: 100px; /* 给演示元素指定宽度 */
  height: 100px; /* 给演示元素指定高度 */
  background-color: green;
  animation: over-and-back 1.5s linear 3; /* 给元素设置动画特效 */
}

在浏览器中打开页面,会看到示例动画重复执行了三遍方才停止。

animation 属性是好几个 CSS 属性的简写形式,在本例中,我们其实设置了以下四个样式属性:

animation-name(即 over-and-back)—— 代表动画名称,正如 @keyframes 规则定义的那样。
animation-duration(即 1.5s)—— 代表动画持续的时长,在本例中,即持续 1.5s。
animation-timing-function(即 linear)—— 代表定时函数,用于描述动画的加速与(或)减速过程。其属性值既可以是贝赛尔曲线,也可以是某个关键字,正如 CSS 过渡用到的定时函数那样(ease-in、ease-out 等等)。
animation-iteration-count(即 3)—— 代表动画重复的次数。其初始值默认为 1。
重新加载页面查看动画效果,观察动画执行过程中的这几个地方:

第一,颜色从 0% 的红色平滑过渡到 100% 的浅紫色,但是接下来动画重复的时候立即变回红色。如果需要重复某个动画并希望整体衔接流畅,需要确保结束值与初始值相匹配。

提示

您也可以使用关键字 from 和 to 来分别代替起始和结束关键帧的 0% 与 100%。稍后还会演示一个该写法的案例(详见代码清单 17.12)。

其次,在最后一次重复动画结束后,背景色变为绿色,即原样式规则中指定的值。但注意观察,在动画持续过程中,该样式声明被 @keyframes 中的规则覆盖了。如果出现样式层叠,那么动画中设置的规则将会比其他声明拥有更高的优先级。

回顾一下第一章(详见第 1.1.1 节)介绍的知识,层叠规则的第一部分就介绍了样式表的来源。作者样式之所以优先级高于用户代理样式,是因为作者样式具有比较高的优先级来源;而动画中应用的声明还拥有比它更高的优先级来源。这样,在为某个属性添加动画时,动画样式就会覆盖样式表中其他位置指定的样式。这就确保了关键帧中所有的声明可以相互配合完成动画,而不用关注动画之外该元素都设置了哪些样式。

17.2 3D 变换下的动画设置 Animating 3D transforms
接下来,我们继续对上一章留下的示例页添加动画特效。在完成代码清单 17.11 后,我们就有了一个蓝色背景的页面,页面左侧为导航菜单。我们将用几张内容卡片来填充页面剩余的部分。先来完成整个效果图的页面布局,然后再设置动画。

17.2.1 添加动画前页面布局的构建 Building the layout without animations
在这个演示中,我们会在页面主区域添加一些卡片(如图 17.3 所示)。然后再使用 3D 变换添加动画,让卡片具有飞入效果。

【图 17.3 页面主区域添加的内容卡片效果图】

代码清单 17.4 展示了这部分内容的 HTML 标记。将这些代码添加到页面的 <nav> 元素后面(为了节省空间,代码中卡片内的文字有删减。如果想要更加接近图 17.3 所示的效果,可以随意添加更多内容)。

代码清单 17.4 创建 flyin-grid 部分以及另几张卡片的 HTML 标记

<main class="flyin-grid"><!-- 网格容器 -->
  <div class="flyin-grid__item card"><!-- 内容卡片,同时也是网格元素 -->
    <img src="images/chicken1.jpg" alt="a chicken"/>
    <h4>Mrs. Featherstone</h4>
    <p>
      She may be a bit frumpy, but Mrs. Featherstone gets
      the job done. She lays her largish cream-colored
      eggs on a daily basis. She is gregarious to a fault.
    </p>
  </div>
  <div class="flyin-grid__item card"><!-- 内容卡片,同时也是网格元素 -->
    <img src="images/chicken2.jpg" alt="a chicken"/>
    <h4>Hen Solo</h4>
    <p>
      Though the most recent addition to our flock, Hen
      Solo is a fast favorite among our laying brood.
    </p>
  </div>
  <div class="flyin-grid__item card"><!-- 内容卡片,同时也是网格元素 -->
    <img src="images/chicken3.jpg" alt="a chicken"/>
    <h4>Cluck Norris</h4>
    <p>
      Every brood has its brawler. Cluck Norris is our
      feistiest hen, frequently picking fights with other
      hens about laying territory and foraging space.
    </p>
  </div>
  <div class="flyin-grid__item card"><!-- 内容卡片,同时也是网格元素 -->
    <img src="images/chicken4.jpg" alt="a chicken"/>
    <h4>Peggy Schuyler</h4>
    <p>
      Peggy was our first and friendliest hen. She is the
      most likely to greet visitors to the yard, and
      frequently to be found nesting in the coop.
    </p>
  </div>
</main>

这部分页面由两个模块组成。外层模块是飞入网格(flyin-grid),为网格内的元素提供了布局,同时还包含了稍后即将介绍的 3D 飞入效果。每个网格元素都是内层卡片模块的一个实例。卡片模块则提供了外观样式,包括白色背景、内边距和字体颜色。

这种页面的布局首推网格布局,接下来就会用到。我们将同样率先实现移动端布局,然后为更大尺寸的视口添加基于网格布局的页面样式。

移动端布局的页面效果如图 17.4 所示。对于小尺寸屏,内容卡片将填满屏幕整个宽度,只留出一点左右外边距。

【图 17.4 在移动端布局中,卡片会填满屏幕宽度,依次叠放在导航菜单下方】

请将代码清单 17.5 所示的移动端样式代码,添加到本地样式表的 modules 模块图层中。

代码清单 17.5 内容卡片的移动端示例样式代码

@layer modules {
  .flyin-grid {
    margin-inline: 1rem; /* 在容器左右两侧添加一段很小的外边距 */
  }

  .card {
    margin-block-end: 1em;
    /* 为卡片添加颜色和其他细节样式: */
    padding: 0.5em;
    background-color: white;
    color: oklch(32% 0.02 248deg);
    box-shadow: 3px 8px 15px rgb(0 0 0 / 0.3);
  }
  .card > img {
    width: 100%; /* 令图片占满内容卡片的全部宽度 */
  }
}

在这样的屏幕尺寸下,flyin-grid 部分很轻松就能实现,因为网格元素只需要像常规块级元素那样正确堆叠即可。每张卡片都设置了白色背景和简单的外观样式。稍后我们就会利用媒体查询设置更复杂的布局。

接下来需要对屏幕尺寸更大的断点设置网格布局。设置完毕后的页面效果看起来会和最终设计图非常接近(如图 17.3 所示)。请根据代码清单 17.6 同步更新本地样式表。

代码清单 17.6 网格布局的示例样式代码

.flyin-grid {
  margin-inline: 1rem;
}

@media (min-width: 30em) {
  .flyin-grid {
    margin-inline: 5rem;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); /* 定义列宽 */
    gap: 2em;
  }
}

上述代码中,网格列(grid columns)将确保所有的网格元素宽度相同。使用 repeat() 和 auto-fit 关键字可以让网格自行决定当前视口宽度下最合适的列数是多少;而在小尺寸视口中仍将显示更为简单的移动端布局。

17.2.2 为布局添加动画 Adding animation to the layout
既然页面的设计与布局工作已经完成,接下来就该添加动画了。页面加载时,卡片是带飞入特效的,如图 17.5 所示。卡片会以围绕纵轴旋转 90 度的初始状态,从页面远端出现。然后它们会飞向观众,并在动画行将结束时旋转到直接朝向观众。图 17.5 展示了定义该动画特效的三个关键帧。

【图 17.5 使用 3D 变换实现从远端飞入内容卡片的效果图】

这段动画包含两个 CSS 变换:translateZ() 用于让卡片从远端飞回页面,而 rotateY() 负责旋转卡片。代码清单 17.7 列出了相关样式代码,其中包括对 flyin-grid 容器的透视距离的设置、关键帧的定义,还为每个 flyin-grid 元素指定了动画特效。此外,我们还添加了不透明度 opacity 的相关设置,这样元素飞入时就会带有过渡特效。

代码清单 17.7 添加飞入动画的示例样式代码

.flyin-grid {
  margin-inline: 1rem;
  perspective: 500px; /* 在容器上设置统一的透视距离 */
}
.flyin-grid__item {
  animation: fly-in 600ms ease-in; /* 为每个元素添加动画特效 */
}

@keyframes fly-in {
  0% {
    transform: translateZ(-800px) rotateY(90deg); /* 旋转后,从远处开始动画 */
    opacity: 0;
  }
  56% {
    transform: translateZ(-160px) rotateY(87deg); /* 已经很接近了,但卡片大部分仍是旋转状态 */
    opacity: 1;
  }
  100% {
    transform: translateZ(0) rotateY(0); /* 在正常位置结束动画 */
  }
}

上述 CSS 样式在容器上设置了透视距离,这样所有的元素都会处于相同的透视场景中;同时还为每个元素设置了动画特效。重新加载页面查看动画效果。

动画开始时将旋转了 90 度的元素放回远处。在起始关键帧和中间关键帧之间,元素沿 z 轴一路向前快速推进(从 800px 来到 160px),并且从透明状态逐渐过渡到完全不透明。从中间关键帧到结束关键帧,最后一小段推进结束,同时大部分的旋转动画发生在这段时间内。