使用 CSS 动画

CSS 动画使得从一种 CSS 样式配置到另一种的过渡动画成为可能。动画由两个组件组成:描述 CSS 动画的样式和一组关键帧,这些关键帧指示动画样式的开始和结束状态,以及可能的中间路径点。

与传统的脚本驱动动画技术相比,CSS 动画有三个主要优势

  1. 它们对于基本动画来说易于使用;你甚至无需了解 JavaScript 即可创建它们。
  2. 即使在适度的系统负载下,动画也能很好地运行。简单的动画在 JavaScript 中通常表现不佳。渲染引擎可以使用跳帧和其他技术来尽可能保持性能流畅。
  3. 让浏览器控制动画序列可以使浏览器优化性能和效率,例如,通过降低当前不可见标签页中运行的动画的更新频率。

配置动画

要创建 CSS 动画序列,你可以使用 animation 属性或其子属性来为要动画化的元素设置样式。这使你可以配置动画序列应如何进行的计时、持续时间和其他详细信息。这不会配置动画的实际外观,动画的实际外观是使用 @keyframes at-rule 完成的,如下面使用关键帧定义动画序列部分所述。

animation 属性的子属性是

animation-composition

指定当多个动画同时影响同一属性时要使用的合成操作。此属性不是 animation 简写属性的一部分。

animation-delay

指定元素加载与动画序列开始之间的延迟,以及动画是应该从头开始立即开始还是从动画的中间开始。

animation-direction

指定动画的第一次迭代是向前还是向后,以及后续迭代是应该在每次运行序列时交替方向还是重置到起点并重复。

animation-duration

指定动画完成一个周期的时长。

animation-fill-mode

指定动画在运行前后如何将其样式应用于目标。

注意:对于动画 forwards 填充模式,动画属性的行为如同包含在一组 will-change 属性值中。如果在动画期间创建了新的堆叠上下文,则目标元素在动画结束后会保留堆叠上下文。

animation-iteration-count

指定动画应重复的次数。

animation-name

指定描述动画关键帧的 @keyframes at-rule 的名称。

animation-play-state

指定是暂停还是播放动画序列。

animation-timeline

指定用于控制 CSS 动画进度的时间线。

animation-timing-function

通过建立加速度曲线来指定动画如何在关键帧之间过渡。

使用关键帧定义动画序列

配置好动画的计时后,你需要定义动画的外观。这是通过使用 @keyframes at-rule 建立一个或多个关键帧来完成的。每个关键帧描述了动画元素在动画序列的给定时间应如何渲染。

由于动画的计时是在配置动画的 CSS 样式中定义的,因此关键帧使用 <percentage> 来指示它们在动画序列中发生的时间。0% 表示动画序列的第一个时刻,而 100% 表示动画的最终状态。由于这两个时间非常重要,它们有特殊的别名:fromto。两者都是可选的。如果未指定 from/0%to/100%,浏览器会使用所有属性的计算值来开始或结束动画。

你可以选择包含其他关键帧,这些关键帧描述了动画开始和结束之间的中间步骤。

使用动画简写

animation 简写对于节省空间很有用。例如,本文中我们使用的一些规则

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

...可以使用 animation 简写代替。

css
p {
  animation: 3s infinite alternate slide-in;
}

要了解有关在使用 animation 简写时可以指定不同动画属性值的顺序的更多信息,请参阅 animation 参考页面。

设置多个动画属性值

CSS 动画的非简写属性可以接受多个值,用逗号分隔。当你想在一个规则中应用多个动画并为每个动画设置不同的持续时间、迭代次数等时,可以使用此功能。让我们看一些快速示例来解释不同的排列。

在第一个示例中,有三个持续时间和三个迭代次数值。因此,每个动画都分配了一个持续时间和迭代次数值,其位置与动画名称相同。fadeInOut 动画的持续时间为 2.5s,迭代次数为 2,而 bounce 动画的持续时间为 1s,迭代次数为 5

css
animation-name: fadeInOut, moveLeft300px, bounce;
animation-duration: 2.5s, 5s, 1s;
animation-iteration-count: 2, 1, 5;

在第二个示例中,设置了三个动画名称,但只有一个持续时间和迭代次数。在这种情况下,所有三个动画都具有相同的持续时间和迭代次数。

css
animation-name: fadeInOut, moveLeft300px, bounce;
animation-duration: 3s;
animation-iteration-count: 1;

在第三个示例中,指定了三个动画,但只有两个持续时间和迭代次数。在这种情况下,如果列表中没有足够的值来为每个动画分配一个单独的值,则值分配会从可用列表中的第一个项目循环到最后一个项目,然后循环回第一个项目。因此,fadeInOut 的持续时间为 2.5smoveLeft300px 的持续时间为 5s,这是持续时间值列表中的最后一个值。现在持续时间值分配重置为第一个值;因此,bounce 的持续时间为 2.5s。迭代次数值(以及你指定的任何其他属性值)将以相同的方式分配。

css
animation-name: fadeInOut, moveLeft300px, bounce;
animation-duration: 2.5s, 5s;
animation-iteration-count: 2, 1;

如果动画数量和动画属性值之间的不匹配是颠倒的,例如三个 animation-name 值有五个 animation-duration 值,那么在这种情况下,额外或未使用的动画属性值(例如两个 animation-duration 值)不适用于任何动画并且会被忽略。

示例

使文本在浏览器窗口中滑动

这个基本示例使用 translatescale 过渡属性为 <p> 元素设置样式,使文本从浏览器窗口右边缘滑入。

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
}

@keyframes slide-in {
  from {
    translate: 150vw 0;
    scale: 200% 1;
  }

  to {
    translate: 0 0;
    scale: 100% 1;
  }
}

在此示例中,<p> 元素的样式指定动画应使用 animation-duration 属性从开始到结束执行 3 秒,并且定义动画序列关键帧的 @keyframes at-rule 的名称为 slide-in

在这种情况下,我们只有两个关键帧。第一个发生在 0%(使用别名 from)。在这里,我们将元素的 translate 属性配置为 150vw(即超出包含元素的极右边缘),并将元素的 scale 配置为 200%(或其默认内联大小的两倍),导致段落的宽度是其 <body> 包含块的两倍。这导致动画的第一帧将标题绘制在浏览器窗口的右边缘之外。

第二个关键帧发生在 100%(使用别名 to)。translate 属性设置为 0%,元素的 scale 设置为 1,即 100%。这导致标题在其默认状态下完成动画,与内容区域的左边缘齐平。

html
<p>
  The Caterpillar and Alice looked at each other for some time in silence: at
  last the Caterpillar took the hookah out of its mouth, and addressed her in a
  languid, sleepy voice.
</p>

注意:重新加载页面以查看动画。

添加另一个关键帧动画

让我们为上一个示例的动画添加另一个关键帧。假设我们想让 Alice 的名字变成粉红色并变大,然后随着它从右向左移动,再缩小回原始大小和颜色。虽然我们可以更改 font-size,但更改任何影响盒模型的属性都会对性能产生负面影响。相反,我们将她的名字包装在 <span> 中,然后单独缩放并为其分配颜色。这需要添加另一个只影响 <span> 的动画

css
@keyframes grow-shrink {
  25%,
  75% {
    scale: 100%;
  }

  50% {
    scale: 200%;
    color: magenta;
  }
}

完整的代码现在看起来像这样

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
}
p span {
  display: inline-block;
  animation-duration: 3s;
  animation-name: grow-shrink;
}

@keyframes slide-in {
  from {
    translate: 150vw 0;
    scale: 200% 1;
  }

  to {
    translate: 0 0;
    scale: 100% 1;
  }
}

@keyframes grow-shrink {
  25%,
  75% {
    scale: 100%;
  }

  50% {
    scale: 200%;
    color: magenta;
  }
}

我们在“Alice”周围添加了一个 <span>

html
<p>
  The Caterpillar and <span>Alice</span> looked at each other for some time in
  silence: at last the Caterpillar took the hookah out of its mouth, and
  addressed her in a languid, sleepy voice.
</p>

这告诉浏览器,在动画的第一个和最后一个 25% 中,名字应该是正常的,但在中间放大和缩小并变为粉红色。我们将 span 的 display 属性设置为 inline-block,因为 transform 属性不影响非替换的内联级别内容

注意:重新加载页面以查看动画。

重复动画

要使动画重复,请使用 animation-iteration-count 属性指示动画重复的次数。在这种情况下,让我们使用 infinite 来使动画无限重复

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: infinite;
}

使动画来回移动

这使其重复了,但每次开始动画时都跳回开头非常奇怪。我们真正想要的是让它在屏幕上来回移动。通过将 animation-direction 设置为 alternate,这很容易实现

css
p {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

使用动画事件

通过利用动画事件,你可以获得对动画的额外控制以及有用的信息。这些事件由 AnimationEvent 对象表示,可用于检测动画何时开始、结束和开始新的迭代。每个事件都包括它发生的时间以及触发事件的动画名称。

我们将修改滑动文本示例,以在每个动画事件发生时输出一些关于它的信息,以便我们可以了解它们是如何工作的。

我们包含了与上一个示例相同的关键帧动画。此动画将持续 3 秒,名为“slide-in”,重复 3 次,并且每次都以交替方向移动。在 @keyframes 中,缩放和转换沿 x 轴操纵,使元素在屏幕上滑动。

css
.slide-in {
  animation-duration: 3s;
  animation-name: slide-in;
  animation-iteration-count: 3;
  animation-direction: alternate;
}

添加动画事件监听器

我们将使用 JavaScript 代码监听所有三种可能的动画事件。此代码配置我们的事件监听器;我们会在文档首次加载时调用它来设置一切。

js
const element = document.getElementById("watch-me");
element.addEventListener("animationstart", listener);
element.addEventListener("animationend", listener);
element.addEventListener("animationiteration", listener);

element.className = "slide-in";

这是相当标准的代码;你可以在 eventTarget.addEventListener() 的文档中获取有关其工作原理的详细信息。此代码做的最后一件事是将我们动画元素的 class 设置为“slide-in”;我们这样做是为了启动动画。

为什么?因为 animationstart 事件在动画开始时立即触发,在我们的例子中,这发生在我们的代码运行之前。所以我们将通过将元素的类设置为之后动画的样式来启动动画。

接收事件

事件将传递到下面显示的 listener() 函数。

js
function listener(event) {
  const l = document.createElement("li");
  switch (event.type) {
    case "animationstart":
      l.textContent = `Started: elapsed time is ${event.elapsedTime}`;
      break;
    case "animationend":
      l.textContent = `Ended: elapsed time is ${event.elapsedTime}`;
      break;
    case "animationiteration":
      l.textContent = `New loop started at time ${event.elapsedTime}`;
      break;
  }
  document.getElementById("output").appendChild(l);
}

这段代码也很简单。它查看 event.type 以确定发生了哪种动画事件,然后将适当的注释添加到我们用于记录这些事件的 <ul>(无序列表)中。

最终输出结果看起来像这样

  • 已启动:经过时间为 0
  • 新循环在时间 3.01200008392334 开始
  • 新循环在时间 6.00600004196167 开始
  • 已结束:经过时间为 9.234000205993652

请注意,这些时间非常接近,但不完全是,动画配置时建立的预期时间。另请注意,在动画的最后一次迭代之后,不会发送 animationiteration 事件;相反,会发送 animationend 事件。

为了完整起见,这里是显示页面内容的 HTML,包括脚本插入有关接收到的事件信息的列表

html
<h1 id="watch-me">Watch me move</h1>
<p>
  This example shows how to use CSS animations to make <code>H1</code>
  elements move across the page.
</p>
<p>
  In addition, we output some text each time an animation event fires, so you
  can see them in action.
</p>
<ul id="output"></ul>

这是实时输出。

注意:重新加载页面以查看动画。

动画化 display 和 content-visibility

此示例演示了如何动画化 displaycontent-visibility。此行为对于创建进入/退出动画很有用,例如,当你希望使用 display: none 从 DOM 中删除容器,但希望它使用 opacity 平滑淡出而不是立即消失时。

支持的浏览器会使用 离散动画类型的变体来动画化 displaycontent-visibility。这通常意味着属性将在两个值之间动画的 50% 处在两个值之间翻转。

但是,有一个例外,那就是当动画从 display: nonecontent-visibility: hidden 到可见值时。在这种情况下,浏览器将在两个值之间翻转,以便动画内容在整个动画持续时间内显示。

所以例如:

  • displaynone 动画到 block(或其他可见的 display 值)时,该值将在动画持续时间的 0% 处切换到 block,使其在整个过程中可见。
  • displayblock(或其他可见的 display 值)动画到 none 时,该值将在动画持续时间的 100% 处切换到 none,使其在整个过程中可见。

HTML

HTML 包含两个 <p> 元素,中间有一个 <div>,我们将从 display none 动画到 block

html
<p>
  Click anywhere on the screen or press any key to toggle the
  <code>&lt;div&gt;</code> between hidden and showing.
</p>

<div>
  This is a <code>&lt;div&gt;</code> element that animates between
  <code>display: none; opacity: 0</code> and
  <code>display: block; opacity: 1</code>. Neat, huh?
</div>

<p>
  This is another paragraph to show that <code>display: none;</code> is being
  applied and removed on the above <code>&lt;div&gt; </code>. If only its
  <code>opacity</code> was being changed, it would always take up the space in
  the DOM.
</p>

CSS

css
html {
  height: 100vh;
}

div {
  font-size: 1.6rem;
  padding: 20px;
  border: 3px solid red;
  border-radius: 20px;
  width: 480px;
  opacity: 0;
  display: none;
}

/* Animation classes */

div.fade-in {
  display: block;
  animation: fade-in 0.7s ease-in forwards;
}

div.fade-out {
  animation: fade-out 0.7s ease-out forwards;
}

/* Animation keyframes */

@keyframes fade-in {
  0% {
    opacity: 0;
    display: none;
  }

  100% {
    opacity: 1;
    display: block;
  }
}

@keyframes fade-out {
  0% {
    opacity: 1;
    display: block;
  }

  100% {
    opacity: 0;
    display: none;
  }
}

请注意在关键帧动画中包含了 display 属性。

JavaScript

最后,我们包含一些 JavaScript 来设置事件监听器以触发动画。具体来说,当我们需要 <div> 显示时,我们添加 fade-in 类,当我们需要它消失时,我们添加 fade-out

js
const divElem = document.querySelector("div");
const htmlElem = document.querySelector(":root");

htmlElem.addEventListener("click", showHide);
document.addEventListener("keydown", showHide);

function showHide() {
  if (divElem.classList[0] === "fade-in") {
    divElem.classList.remove("fade-in");
    divElem.classList.add("fade-out");
  } else {
    divElem.classList.remove("fade-out");
    divElem.classList.add("fade-in");
  }
}

结果

代码渲染如下:

另见