Skip to content
This page has been auto-translated and may contain errors.View in English

过渡和动画

docs.scrimba.com

你给按钮添加一个 :hover 颜色,它确实有效,但变化很生硬:一帧是蓝色,下一帧是深蓝色,中间什么都没有。一个细微的淡入淡出会让整个页面感觉不那么生硬。这种淡入淡出就是过渡,一旦你能描述两个状态间的运动,同样的工具就能扩展到悬停效果、浮起的卡片,以及加载时旋转的微调器。本章讲述 CSS 如何移动事物,以及如何在不让页面变得更难用的情况下移动它们。

过渡

**过渡**告诉浏览器在属性从旧值变为新值时对其进行动画处理,而不是生硬地在它们之间切换。你描述两个状态,正常状态和悬停状态,过渡填充中间的帧。

这是一个在悬停时改变颜色的按钮。没有过渡时颜色会生硬跳变;有过渡时会淡入淡出:

css
.btn {
  background: #3b82f6;
  transition: background 0.2s;   /* 在 0.2 秒内淡入淡出颜色 */
}
.btn:hover {
  background: #1d4ed8;           /* 动画化到的新状态 */
}

你在正常状态上放置 transition,而不是在 :hover 上。这样当鼠标到达时和离开时都会应用。

**过渡**在属性值改变时对其进行动画处理,通常是因为 :hover:focus 这样的状态触发了,或者添加了一个类。你不是逐帧描述动画;你命名属性和持续时间,浏览器插值中间的值。

transition 简写有四个部分,按此顺序:要监视的属性、持续时间、时间函数(速度曲线)和可选的延迟。

css
.btn {
  background: #3b82f6;
  transition: background 0.2s ease-in-out 0s;
  /* 属性 | 持续时间 | 时间函数 | 延迟 */
}
.btn:hover {
  background: #1d4ed8;
}

你可以通过用逗号分隔多个属性来同时过渡它们,并在基础状态上声明过渡,这样可以覆盖进入和离开悬停。

css
.card {
  transition: transform 0.2s, box-shadow 0.2s;   /* 两个属性,相同的时间 */
}

**过渡**在计算值改变时在一段时间内插值属性的起始值和终止值。触发器是该属性的任何改变:伪类如 :hover、从 JavaScript 切换的类,或内联样式编辑。四个子属性是 transition-propertytransition-durationtransition-timing-functiontransition-delay,简写按属性、持续时间、时间、延迟的顺序排列。

两个约束决定过渡是否会运行。属性必须是可动画化的,意味着它在两个值之间有定义的插值:background-coloropacitytransform 是可动画化的,而像 display 这样的属性是离散的,历史上是生硬切换而不是补间。起始值和终止值都必须是具体的:你不能过渡到或从 auto 过渡,这就是为什么动画化 heightauto 没有任何作用,人们转而使用 max-heighttransform

css
.btn {
  background: #3b82f6;
  transition: background-color 0.2s ease-in-out;   /* 可动画化,两个具体值 */
}
.btn:hover {
  background: #1d4ed8;
}

在基础规则上声明过渡,而不是在 :hover 规则上。生效的过渡属性从元素的当前状态读取,所以在基础状态上放置它意味着它控制到达和返回;仅在 :hover 上放置它会动画化进入和生硬地离开。

Juno过渡 过渡平滑地处理一个变化而不是生硬地处理,就像在悬停时淡入淡出按钮的颜色。你在正常状态上写它,而不是在 :hover 上,所以它会往返工作。从 属性持续时间开始,你已经有了一个有效的淡入淡出。
Juno过渡transition 简写读取属性、持续时间、时间函数、延迟,你通过用逗号分隔来列出多个。在基础规则上放置它,以便覆盖进入和离开状态。如果悬停进入时看起来平滑但离开时跳变,过渡就在 :hover 上而不是基础上,这个问题经常发生。
Juno过渡 过渡仅在具有两个具体终止值的可动画化属性上运行,这就是为什么过渡到 height: auto 无声地什么都不做。在基础状态上保持声明,以便它控制两个方向而不是在返回时生硬地处理。一旦你知道四个子属性,简写就只是它们的顺序:属性、持续时间、时间、延迟。

变换

**变换**移动、缩放或旋转元素,而不会扰乱周围的任何东西。这是你通常想要动画化的东西,因为它看起来光滑,不会把邻居推到一边。

有三个你会经常使用的:

css
.card:hover {
  transform: translateY(-4px);   /* 向上移动 4 像素 */
}
.icon:hover {
  transform: scale(1.1);         /* 放大到其大小的 110% */
}
.arrow.open {
  transform: rotate(90deg);      /* 顺时针旋转四分之一圈 */
}

translate 移动,scale 调整大小,rotate 旋转。将其中任何一个与过渡配对,你就会得到一个浮起的卡片、一个突出的图标,或一个摇摆打开的箭头。

transform 对元素应用视觉更改,移动、调整大小或旋转它,而不改变其在布局中的位置。最后一部分是关键:变换的元素在新位置绘制,但它原始占据的空间保持保留,所以其邻居不会重排。这就是为什么你为移动动画 transform 而不是 topmargin

你最常用的三个是 translate()scale()rotate(),你可以在一个声明中组合它们:

css
.card {
  transition: transform 0.2s ease-out;
}
.card:hover {
  transform: translateY(-4px) scale(1.02);   /* 浮起并略微放大 */
}

函数从左到右应用,transform-origin 设置它们围绕的点,默认为元素的中心。改为 top leftrotate 就会从角落摇摆。

css
.badge {
  transform-origin: top left;
  transform: rotate(-8deg);
}

transform 在其自己的绘制图层中重新定位元素,不影响文档流:盒子的原始空间保持保留,所以没有兄弟元素重排,当变换改变时没有布局重新计算。这就是使 transform 成为动画的正确选择的属性,下面关于性能的深度阅读精确说明了这一点。

translate()scale()rotate() 是常见函数,顺序很重要,因为它们作为从右到左有效应用的矩阵乘法组合:translateX(50px) rotate(45deg) 先移动然后旋转移动的元素,而反向先旋转然后沿旋转的轴进行平移,给出不同的结果。transform-origin 设置 scalerotate 的枢轴,默认为元素的中心(50% 50%)。

css
.panel {
  transform-origin: top center;
  transition: transform 0.25s ease-out;
}
.panel.collapsed {
  transform: scaleY(0);          /* 向上边缘折叠 */
}

一个实际约束:transform 仅应用于可以以这种方式定位的盒子,所以它对内联元素(如裸露的 <span>)没有效果。首先给元素 display: inline-blockblock。如果你也在定位元素,请注意变换如何与定位上下文交互,因为变换的元素成为其绝对定位后代的包含块。

Juno变换transform 移动、缩放或旋转元素而不推动其邻居。要记住的三个是 translate 用于移动、scale 用于调整大小,和 rotate 用于旋转。将它与过渡结合,你就会得到一个在悬停时浮起的卡片或一个突出的图标。
Juno变换transform 改变元素的外观而不改变它在布局中的位置,所以邻居永远不会重排,这就是为什么你动画化它而不是 topmargin。在一个声明中堆叠 translatescalerotate,它们从左到右应用。当你想要一个旋转或缩放围绕除中心之外的某处枢轴时,使用 transform-origin
Juno变换 变换函数按顺序组合,所以 translateX(50px) rotate(45deg) 与反向不同,transform-origin 设置旋转或缩放的枢轴。在裸露的内联元素上它什么都不做,直到你使它 inline-block。记住它也将元素转换为绝对定位子元素的包含块,这可能会在以后让你惊讶。

关键帧动画

过渡在两个状态间动画化。当你需要超过两个状态,或运动需要重复时,你使用**关键帧动画**。你用 @keyframes 写出步骤,然后用 animation 属性将它们附加到元素上。

加载微调器是经典示例:它永久旋转,所以过渡无法做到。

css
@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.spinner {
  animation: spin 1s linear infinite;   /* 运行 spin,每转 1 秒,永久 */
}

from 是开始,to 是结束。animation 行说明要运行哪些关键帧、一个周期花多长时间、速度曲线以及重复多少次。infinite 意味着它永远不会停止。

@keyframes 定义一个命名的步骤序列,animation 属性在元素上运行它。与过渡不同,动画不需要状态改变来触发它,它自己启动,它可以有尽可能多的步骤,使用从 0%100% 的百分比。

css
@keyframes pulse {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.05); }   /* 中点步骤 */
  100% { transform: scale(1); }
}
.badge {
  animation: pulse 1.5s ease-in-out infinite;
}

animation 简写打包几个子属性:名称、持续时间、时间函数、延迟、迭代计数和方向。infinite 永远循环,一个数字运行那么多次,alternate 作为方向使每个周期反向,所以运动来回运行而不是跳回开始。

css
.spinner {
  animation: spin 1s linear infinite;
}

@keyframes 声明一个命名时间轴,其属性值在表示为百分比的点上(fromto0%100% 的别名),animation 将该时间轴绑定到元素。动画是自触发的并可以循环,这就是它与过渡的界线:使用过渡来平滑状态改变,使用动画来自己运行或重复的运动。

animation 简写包含八个子属性:animation-name-duration-timing-function-delay-iteration-count-direction-fill-mode-play-state。其中两个困扰人们。animation-fill-mode: forwards 使元素在动画结束后保持其最后一个关键帧,而不是生硬地恢复到其无动画样式,这是一次性进场想要的。并且时间函数应用于每个关键帧段,而不是整个动画,所以三步动画上的 ease-in-out 在每个段进出缓动,而不是一次端对端。

css
@keyframes slide-in {
  from { opacity: 0; transform: translateX(-16px); }
  to   { opacity: 1; transform: translateX(0); }
}
.toast {
  animation: slide-in 0.3s ease-out forwards;   /* 保持最后状态 */
}

优先在关键帧内动画化 transformopacity,出于与过渡相同的原因,下一部分解开这一点:那两个属性是最便宜的动画化属性,大多数其他的不是。

Juno关键帧动画 当两个状态不够,或运动需要重复时,在 @keyframes 中写出步骤并用 animation 运行它们。微调器是最好的例子:from 在 0 度,to 在 360,用 infinite 循环。animation 行设置哪些关键帧、花多长时间、曲线以及多少次。
Juno关键帧动画@keyframes 将步骤作为百分比保存,animation 运行它们,不需要状态改变。简写打包名称、持续时间、时间、延迟、迭代计数和方向,所以 infinite 循环,alternate 使每个周期来回运行。多步运动和任何重复的东西属于这里,不在过渡中。
Juno关键帧动画 时间函数按关键帧段运行,不是一次跨越整个动画,这第一次会让人惊讶当一个缓动的多步运行看起来坎坷。当一次性进场应该保持其最后状态而不是生硬地恢复时,使用 animation-fill-mode: forwards。并尽可能在 transformopacity 上保持关键帧,出于接下来的性能原因。

尊重用户的运动

不是每个人都想要屏幕上的运动。有些人觉得它分散注意力,对一些人来说,它可能会导致真实的不适或恶心。浏览器让人们在系统设置中请求更少的运动,CSS 可以通过**prefers-reduced-motion** 读取该请求。

将你的动画包裹在检查中,以便它们仅对未要求减少运动的人运行:

css
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.2s;
  }
}

尊重用户的另一半是克制:保持运动简短、微妙,并与阅读器做的事情相关,比如悬停或点击。揭示真实改变的运动有帮助;为了运动本身而运动很快就会疲劳。

运动不是中立的。对一些阅读器,动画是一个分散注意力的东西,对有庭院器官条件的人,大的或突然的运动可能引发眩晕或恶心。操作系统暴露一个"减少运动"设置,prefers-reduced-motion 媒体特性让你的 CSS 尊重它。

将运动视为选择加入,而不是选择退出。将平静基线写为你的默认,然后在 no-preference 查询内分层运动:

css
.card {
  transition: none;              /* 对每个人都平静的默认 */
}
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.2s ease-out;
  }
}

超越媒体查询,目的是将好的运动与噪音分开的东西。当运动解释一个改变时它赚取其位置:一个从其锚定的边滑动的菜单,一个淡入淡出的吐司以便它不会从无处出现。快速和微妙读作波兰;缓慢和大读作障碍。

prefers-reduced-motion 是一个媒体特性,反映阅读器在操作系统级别选择的可访问性设置,尊重它是一个基线要求,而不是一个漂亮的东西,因为未被请求的运动可能会导致庭院不适。要达到的模式是将减少的状态作为默认进行创作,并在 @media (prefers-reduced-motion: no-preference) 内启用运动,所以一个浏览器不支持该特性的阅读器仍然得到平静的基线而不是运动。

"减少"很少意味着"移除"。当一个滑动或缩放不是时,淡入淡出或颜色改变通常没问题,所以有用的举动是将大的位置运动交换为小的不透明度改变,而不是杀死所有反馈:

css
.modal {
  animation: slide-in 0.3s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .modal {
    animation: fade-in 0.3s ease-out;   /* 保持反馈,放弃运动 */
  }
}

目的和缓动承载其余的。进场以 ease-out 读最佳,它快速开始然后安定下来,因为元素到达有意图并来到休息;出口通常适合 ease-in。保持持续时间短(大约 150ms 到 300ms 用于界面反馈)和运动小;一个常见的失败是一个漂亮的动画,给每个交互添加四分之一秒的等待。运动应该澄清一个状态改变,而不是装饰一个。

Juno尊重用户的运动 有些人觉得运动分散注意力,甚至在身体上不舒服,他们可以请求浏览器给予更少的运动。用 prefers-reduced-motion 读取它,仅对未要求减少运动的人运行动画。保持你添加的运动短、微妙,并与阅读器做的事情相关。
Juno尊重用户的运动 将平静版本作为你的默认进行创作,并在 no-preference 查询内添加运动,所以运动是选择加入而不是人们必须对抗的东西。好的运动解释一个改变,比如菜单从其锚点滑动;装饰性运动快速变得令人厌倦。快速和小读作波兰,缓慢和大读作障碍。
Juno尊重用户的运动 减少很少意味着移除:交换一个滑动为一个淡入淡出,所以要求更少运动的人仍然得到反馈,并默认为平静状态,所以不支持的浏览器也着陆在那里。对进场使用 ease-out,并保持界面反馈在 150 到 300 毫秒左右。一个给每个点击添加等待的漂亮动画是成本,而不是特性。

动画什么,以及为什么重要

一个会在以后为你节省麻烦的快速规则:当你动画化运动时,动画化 transformopacity,而不是像 widthheighttopmargin 这样的属性。

css
/* 平滑:浏览器廉价处理这些 */
.card:hover {
  transform: translateY(-4px);
  opacity: 0.9;
}

原因是 transformopacity 是浏览器最廉价地移动的两个属性,所以基于它们构建的动画即使在较慢的设备上也保持平滑。动画化大小或位置属性使浏览器每帧做更多工作,运动可能会卡顿。当你深入时你会满足完整的原因,但现在养成习惯是值得的。

有一个原因本章的例子依赖 transformopacity。这两个属性是最便宜的动画化东西,大多数其他的不是。动画化 widthheighttopmargin 强制浏览器每帧重新计算布局,这是昂贵的,并在较慢的设备上显示为卡顿。

实际规则是尽可能将运动表示为 transformopacity。想移动什么东西?使用 translate,而不是 top。想调整其大小?使用 scale,而不是 width。想让它出现?淡入淡出 opacity,不要从零动画化 height

css
/* 昂贵:动画化布局属性 */
.panel {
  transition: height 0.3s;       /* 每帧重新计算布局 */
}

/* 廉价:相同效果,表示为 transform */
.panel {
  transition: transform 0.3s;
  transform: scaleY(0);          /* 没有布局工作 */
}

这回到为什么 transform 不扰乱其邻居:因为它永远不会触及布局,浏览器可以动画化它而不重做页面。如果你发现自己也在寻找颜色或背景过渡,颜色和背景涵盖其中的哪些会插值。

这是之前部分指向的深度阅读:为什么 transformopacity 廉价动画化而几乎所有其他的都不是。它归结为浏览器的渲染管道,分为三个阶段运行。布局计算每个盒子的大小和位置。绘制填充像素:颜色、文本、边框。合成将绘制的图层组合为最后的帧,这是 GPU 可以驱动的阶段。

动画化 transformopacity 可以完全在合成阶段完成。浏览器绘制元素一次,将其提升到其自己的**合成器图层**(一个独立绘制的表面,GPU 可以独立移动和混合),然后每帧仅在新位置或不透明度重新合成该图层。没有布局,没有重绘。动画化 widthtop 代替会使布局无效,强制浏览器每一帧都重新计算几何和重绘,这就是在负载下丢弃帧的原因。

css
/* 仅合成:廉价,GPU 友好的 */
.card {
  transition: transform 0.2s, opacity 0.2s;
}

/* 每帧触发布局和绘制:避免动画化这些 */
.card-heavy {
  transition: width 0.2s, top 0.2s;
}

will-change 让你提示一个属性即将动画化,以便浏览器可以提前提升图层:will-change: transform。吝啬地使用它并在动画完成后移除它,因为每个提升的图层花费内存,提升所有东西会违反目的并可能让页面变慢。将其视为针对测量的卡顿的目标修复,而不是默认。

缓动是值得精确命名的另一个杠杆。时间函数是一条曲线,将经过的时间映射到进度。ease-out(快速然后减速)适合进场,因为元素到达并安定下来;ease-in 适合出场;linear 适合像微调器这样的持续运动。对于任何自定义,cubic-bezier(x1, y1, x2, y2) 通过其两个控制点定义曲线,内置关键字在幕下是命名的三次贝塞尔曲线。记住性能预算:力争保持在帧的时间内(60fps 时约 16ms),动画化仅合成属性,并在达到 will-change 之前达到分析器。如果你从自定义属性驱动其中的多个,你可以在整个界面中保持持续时间和缓动一致。

Juno动画什么,以及为什么重要 动画化 transformopacity,远离动画化 widthheighttopmargin。那两个是浏览器移动最廉价的,所以即使在较慢的设备上运动也保持平滑。你会稍后学习完整的为什么,但现在养成习惯是值得的。
Juno动画什么,以及为什么重要 将运动表示为 transformopacity:使用 translate 而不是 topscale 而不是 width,并淡入淡出 opacity 而不是从零动画化 height。动画化布局属性使浏览器每帧重做布局,这在较慢的设备上卡顿。这是 transform 单独离开邻居的相同原因:它永远不会触及布局。
Juno动画什么,以及为什么重要transformopacity 可以在它们自己的 GPU 图层的合成阶段动画化,所以它们跳过布局和绘制;widthtop 使每帧布局无效,并在负载下丢弃帧。使用 will-change 作为针对测量的卡顿的目标修复并在之后移除它,因为每个提升的图层花费内存。选择缓动以匹配手势,进场用 ease-out,并在优化之前分析。