<译> An Interactive Guide to CSS Transitions

翻译系列第八篇,An Interactive Guide to CSS Transitions,原文地址

建议看原文,里面的带类似CodeSandbox的可以编辑查看效果

基础

我们创建动画所需要的主要元素是一些会变化的CSS,这是一个按钮在鼠标悬停时移动的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<button class="btn">
Hello World
</button>

<style>
.btn {
width: 100px;
height: 100px;
border-radius: 50%;
border: none;
background: slateblue;
color: white;
font-size: 20px;
font-weight: 500;
line-height: 1;
}

.btn:hover {
transform: translateY(-10px);
}
</style>

这段代码使用:hover伪类来指定当用户的鼠标停留在按钮上时的附加CSS声明,类似于JavaScript中的onMouseEnter事件

为了向上移动元素,我们使用transform: translateY(-10px)。虽然我们可以使用margin-top来完成这项工作,但transform: translate是一个更好的工具,我们稍后会看到原因

这种情况下,CSS的变化是即时发生的。眨眼之间按钮已经传送到一个新的位置!这与自然世界是不协调的,在自然世界里,事情是逐渐发生的

我们可以使用恰当命名的transition属性指示浏览器从一个状态插入到另一个状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<button class="btn">
Hello World
</button>

<style>
.btn {
width: 100px;
height: 100px;
border-radius: 50%;
border: none;
background: slateblue;
color: white;
font-size: 20px;
font-weight: 500;
line-height: 1;
}

.btn {
transition: transform 250ms;
}

.btn:hover {
transform: translateY(-10px);
}
</style>

transition有许多值,但是只有两个值是必须的:

  • 动画名称
  • 动画时间

如果希望可以给多个属性增加动画,可以传递一个逗号分隔的列表

1
2
3
4
5
6
7
.btn {
transition: transform 250ms, opacity 400ms;
}
.btn:hover {
transform: scale(1.2);
opacity: 0;
}

Selecting all properties

transition-property有一个特殊值all,使用这个时任何CSS的改变都会被增加过渡(be transitioned),看起来很棒但是非常不建议使用!

计时函数

当我们告诉一个元素从一个位置转换到另一个位置时,浏览器需要计算出每个“中间”帧应该是什么样子

例如:假设我们将一个元素从左向右移动,持续时间为1秒。一个流畅的动画应该以60帧/秒的速度运行,这意味着我们需要在开始和结束之间设置60个独立的位置

为了弄清楚这里发生了什么:每个褪色的圆圈代表一个时间的时刻。当圆圈从左向右移动时,这些就是显示给用户的帧。这就像一本手翻书(flipbook)

在这个动画中,我们使用了一个线性计时函数。这意味着元素以恒定的速度移动;我们的圆圈每帧的移动量是相同的

在CSS中有几个计时函数可供我们使用。我们可以通过transition-timing-function属性指定我们想要使用哪一个

1
2
3
4
.btn {
transition: transform 250ms;
transition-timing-function: linear;
}

或者,我们可以直接将它传递给transition简写属性

1
2
3
.btn {
transition: transform 250ms linear;
}

线性模式并不是最佳选择,毕竟现实世界中几乎没有任何东西会以这种方式运行。好的动画是模仿自然世界的,所以我们应该选择一些更有机的东西

ease-out

如果我们要画出元素随时间的位移,它看起来像这样

它最常用于从屏幕外进入的东西(例如:出现弹窗)。它会产生一种效果,就好像有什么东西从很远的地方突然出现在用户面前

ease-in

(后面扩展到了使用贝塞尔函数,略)

动画表演

前面我们提到过动画应该以60帧每秒的速度运行。当我们进行计算时,我们意识到这意味着浏览器只有16.6毫秒的时间来绘制每一帧。然而大部分时候并不是这样的,作为参考,我们眨眼大约需要100ms-300ms

如果我们的动画在计算上过于昂贵,它就会显得笨拙而断断续续。帧将会下降,因为设备无法跟上

在实践中,糟糕的性能通常会以可变帧率的形式出现

动画表现是一个令人惊讶的深度和有趣的领域,远远超出了这个入门教程的范围。但让我们来讨论一下绝对关键的、需要知道的部分

  • 一些CSS属性在动画上比其他属性要昂贵得多。例如,height是一个非常昂贵的属性,因为它影响布局。当一个元素的高度缩小时,它会引起连锁反应;它的所有兄弟元素也需要向上移动,以填充空间
  • 其他属性(如background-color)的动画成本比较高。它们不会影响布局,但确实需要在每一帧上重新涂上一层颜色,这并不便宜
  • transformopacity这两个属性对动画来说非常便宜。如果一个动画当前调整了一个属性,如widthleft,可以通过使用transform来大大改善性能(尽管它不总是可能达到完全相同的效果)
  • 确保在低性能设备上也能达到相同的效果,你开发的机器可能会很多!

硬件加速

根据你的浏览器和操作系统的不同,您可能会注意到前面的一些示例中有一个奇怪的小缺陷

*

请密切注意这些字母。注意它们是如何在转换开始和结束时出现小故障的,就好像所有的东西都锁定在合适的位置上

这是因为计算机的CPU和GPU之间的切换。让我解释一下

当我们使用transformopacity让一个元素动画时,浏览器有时会尝试优化这个动画。与栅格化每帧像素不同,它将所有内容作为纹理传输给GPU。GPU非常擅长做这些基于纹理的转换,因此,我们得到了一个非常流畅、非常性能的动画。这就是所谓的硬件加速。

这里有个问题:GPU和CPU的渲染稍有不同。当CPU把它交给GPU时(反之亦然),你会看到一些东西在轻微地移动

我们可以通过添加以下CSS属性来解决这个问题

1
2
3
.btn {
will-change: transform;
}

will-change是一个属性,它允许我们提示浏览器我们将对所选元素进行动画化,并且它应该针对这种情况进行优化

实际上,这意味着浏览器会让GPU一直处理这个元素。没有更多的CPU和GPU之间的移交,没有更多的snapping into place

will-change让我们有意识地决定哪些元素应该被硬件加速。浏览器在这方面有自己不可思议的逻辑,我不想让它取决于运气

硬件加速还有另一个好处:我们可以利用亚像素渲染(sub-pixel rendering)

看看下面这两个动画。一个是使用margin-top完成的,所以它不能被硬件加速。请注意两者之间的区别

(例子,看原文)

margin-top这样的属性不能进行亚像素渲染,这意味着它们需要四舍五入到最接近的像素,创建一个阶梯状的效果。同时,由于GPU的抗锯齿技巧,transform可以平滑地在像素之间切换。

注意: 不要滥用GPU加速,因为这相当于把渲染的任务整个交给了GPU,在低端设备上可能会出问题(然后作者在下一段表示他在Xiaomi Redmi 7A上的测试看起来just fine)

可替代属性

事实上,硬件加速已经存在了很长一段时间,比will-change还要长

很长一段时间,它是通过使用3D转换完成的,如transform: translateZ(0px)。即使有一个0px值,浏览器仍然把它交给GPU,因为移动到3D空间绝对是GPU的强项。还有backface-visibility: hidden

will-change出现时,它的目的是给开发人员提供一种适当的、语义上有意义的方式来提示浏览器某个元素应该被优化。然而,在早期,will-change有一些问题

令人高兴的是,似乎所有这些问题都已得到解决。我做了一些测试,发现使用will-change在现代浏览器中获得了最好的结果。但你应该始终自己进行测试,以确保这些技术能够在您的目标设备和浏览器上工作

UX相关

这一部分内容比较多大概就翻译一下

行为驱动动态(Action-driven motion)

(以快速进入和缓慢离开的模态框举例)

我(作者)相信大多数开发人员是根据状态来思考的:例如,你可能会看到这种情况,并说我们有一个悬停状态和一个默认状态。相反,如果我们从行动的角度思考呢?我们根据用户正在做的事情来制作动画,根据事件而不是状态来思考。我们有一个鼠标进入动画和一个鼠标离开动画

这篇博文Meaningfun Motion with Action-Driven Animation.中展示了这个想法如何创造出更高层次的语义意义动画

延时

作者提供了一种使用CSS实现延时的方案

1
2
3
4
5
6
7
8
9
10
.dropdown {
opacity: 0;
transition: opacity 400ms;
transition-delay: 300ms;
}
.dropdown-wrapper:hover .dropdown {
opacity: 1;
transition: opacity 100ms;
transition-delay: 0ms;
}

这个属性可以被简写成transition: opacity 250ms 300ms;,第一个是duration,第二个将delay,但是,作者表示这种模糊规范还不如用明确的声明写法(支持)

毁灭性闪烁

当鼠标靠近元素边界时,就会出现问题。悬停效果将元素从鼠标下方取出,这将导致它回落到鼠标下方,这将导致悬停效果每秒再次触发多次

我们怎么解这个呢?诀窍是把触发和效果分开。这里有一个简单的例子

基础CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.btn {
width: 100px;
height: 100px;
border: none;
background: transparent;
padding: 0px;
}

.background {
display: flex;
align-items: center;
width: 100%;
height: 100%;
border-radius: 50%;
background: slateblue;
color: white;
font-size: 20px;
font-weight: 500;
line-height: 1;
}

关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<button class="btn">
<span class="background">
Hello World
</span>
</button>

<style>
.background {
will-change: transform;
transition: transform 450ms;
}

.btn:hover .background {
transition: transform 150ms;
transform: translateY(-10px);
}

/* Toggle me on for a clue! */
.btn {
/* outline: auto; */
}
</style>

尊重偏好

作者提供了一种禁用动画的方式,因为可能有人不需要动画之类的,可以使用这种方式使得可以进行切换

1
2
3
4
5
@media (prefers-reduced-motion: reduce) {
.btn {
transition: none;
}
}