要点
为剪辑添加动画时,请使用缩放转换。您可以通过对子元素进行计数器缩放,以防止子元素在动画期间被拉伸和倾斜。
我们之前曾发布有关如何创建高性能视差效果和无限滚动条的更新。在本文中,我们将介绍若要实现高性能的剪辑动画,需要满足哪些条件。如果您想观看演示,请查看界面元素示例 GitHub 代码库。
例如,展开式菜单:
构建此容器的某些选项的性能优于其他选项。
错误示例:对容器元素的宽度和高度进行动画处理
您可以想象一下,使用一些 CSS 为容器元素的宽度和高度添加动画效果。
.menu {
overflow: hidden;
width: 350px;
height: 600px;
transition: width 600ms ease-out, height 600ms ease-out;
}
.menu--collapsed {
width: 200px;
height: 60px;
}
此方法的直接问题是,它需要为 width
和 height
添加动画效果。这些属性需要计算布局,并在动画的每一帧上绘制结果,这可能非常耗费资源,并且通常会导致您无法达到 60fps。如果您对此感兴趣,请阅读我们的渲染性能指南,详细了解渲染过程的运作方式。
错误示例:使用 CSS clip 或 clip-path 属性
为 width
和 height
添加动画的替代方法可能是使用(现已废弃的)clip
属性为展开和收起效果添加动画。或者,您也可以改用 clip-path
。但是,使用 clip-path
的支持程度不如 clip
。但 clip
已被废弃。好吧。但不必担心,这并不是您想要的解决方案!
.menu {
position: absolute;
clip: rect(0px 112px 175px 0px);
transition: clip 600ms ease-out;
}
.menu--collapsed {
clip: rect(0px 70px 34px 0px);
}
虽然这种方法比为菜单元素的 width
和 height
添加动画效果要好,但缺点是它仍然会触发绘制。此外,如果您采用该方式,clip
属性会要求其操作的元素采用绝对定位或固定定位,这可能需要稍微调整一下。
良好:为比例添加动画效果
由于此效果涉及到某些内容变大变小,因此您可以使用缩放变换。 这是一个好消息,因为更改转换不需要布局或绘制,并且浏览器可以将其交给 GPU,这意味着效果会加速,并且更有可能达到 60fps。
与渲染性能的大多数方面一样,这种方法的缺点是需要进行一些设置。不过,这完全值得!
第 1 步:计算起始状态和结束状态
如果使用的是采用缩放动画的方法,第一步是读取元素,以了解菜单在收起和展开时的大小。在某些情况下,您可能无法一次性获取这两项信息,而需要切换一些类,才能读取组件的各种状态。不过,如果您需要这样做,请务必谨慎:如果样式自上次运行后发生了更改,getBoundingClientRect()
(或 offsetWidth
和 offsetHeight
)会强制浏览器运行样式和布局传递。
function calculateCollapsedScale () {
// The menu title can act as the marker for the collapsed state.
const collapsed = menuTitle.getBoundingClientRect();
// Whereas the menu as a whole (title plus items) can act as
// a proxy for the expanded state.
const expanded = menu.getBoundingClientRect();
return {
x: collapsed.width / expanded.width,
y: collapsed.height / expanded.height
};
}
对于菜单之类的内容,我们可以做出合理的假设,即它最初将处于其自然缩放比例 (1, 1)。这种自然缩放表示其展开状态,这意味着您需要将动画从缩小的版本(在上面计算)恢复到该自然缩放。
但是先别急!这肯定也会缩放菜单内容,对吗?如您在下方所看到的,是的。
那么,您可以做些什么呢?您可以对内容应用反向转换,例如,如果容器缩小到其正常大小的 1/5,您可以将内容放大 5 倍,以防止内容被压缩。关于这点,有两点需要注意:
反向转换也是一个缩放操作。这很不错,因为它也可以加速,就像容器上的动画一样。您可能需要确保要进行动画处理的元素拥有自己的合成层(以便 GPU 提供帮助),为此,您可以向元素添加
will-change: transform
,或者如果您需要支持旧版浏览器,则可以添加backface-visiblity: hidden
。必须按帧计算反向转换。这时事情可能会变得有点棘手,因为假设动画采用 CSS 并使用缓动函数,则在为反向转换添加动画时,需要对缓动本身进行反向处理。不过,计算
cubic-bezier(0, 0, 0.3, 1)
的反向曲线并不那么明显。
因此,您可能很想考虑使用 JavaScript 来为这种效果添加动画效果。毕竟,您可以使用缓动方程来计算每帧的缩放和反向缩放值。任何基于 JavaScript 的动画的缺点在于,当主线程(运行 JavaScript 的位置)忙于处理其他任务时,会出现什么情况。简而言之,动画可能会卡顿或完全停滞,这对用户体验很不利。
第 2 步:即时构建 CSS 动画
解决方案(起初可能看起来很奇怪)是使用我们自己的缓动函数动态创建关键帧动画,并将其注入页面以供菜单使用。(非常感谢 Chrome 工程师 Robert Flack 指出这一点!)这样做的首要好处是,可在合成器上运行用于更改变形的关键帧动画,这意味着它不会受到主线程上的任务影响。
为了制作关键帧动画,我们从 0 到 100 进行步进,并计算元素及其内容所需的缩放值。然后,这些可以归结为一个字符串,该字符串可以作为样式元素注入到页面中。注入样式会导致页面上进行“重新计算样式”传递,这是浏览器必须执行的额外工作,但它只会在组件启动时执行一次。
function createKeyframeAnimation () {
// Figure out the size of the element when collapsed.
let {x, y} = calculateCollapsedScale();
let animation = '';
let inverseAnimation = '';
for (let step = 0; step <= 100; step++) {
// Remap the step value to an eased one.
let easedStep = ease(step / 100);
// Calculate the scale of the element.
const xScale = x + (1 - x) * easedStep;
const yScale = y + (1 - y) * easedStep;
animation += `${step}% {
transform: scale(${xScale}, ${yScale});
}`;
// And now the inverse for the contents.
const invXScale = 1 / xScale;
const invYScale = 1 / yScale;
inverseAnimation += `${step}% {
transform: scale(${invXScale}, ${invYScale});
}`;
}
return `
@keyframes menuAnimation {
${animation}
}
@keyframes menuContentsAnimation {
${inverseAnimation}
}`;
}
无尽的好奇心可能会好奇 for 循环中的 ease()
函数。您可以使用如下代码将 0 到 1 的值映射到缓和等效项。
function ease (v, pow=4) {
return 1 - Math.pow(1 - v, pow);
}
您也可以使用 Google 搜索绘制示意图。太方便了!如果您需要其他缓动方程,请查看 Soledad Penadés 的 Tween.js,其中包含大量缓动方程。
第 3 步:启用 CSS 动画
创建这些动画并将其以 JavaScript 的形式烧录到网页后,最后一步是切换启用动画的类。
.menu--expanded {
animation-name: menuAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
.menu__contents--expanded {
animation-name: menuContentsAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
这会导致在上一步中创建的动画运行。由于预先烘焙的动画已采用缓动效果,因此需要将时间函数设置为 linear
,否则您将在每个关键帧之间采用缓动效果,这看起来会很奇怪!
如需将元素收起,有两种方法:更新 CSS 动画以向后而不是向前运行。这样做可以正常运行,但动画的“感觉”会相反,因此,如果您使用了缓出曲线,则反向动画会感觉像是缓入,这会使动画看起来缓慢。一种更合适的解决方案是创建用于收起元素的第二对动画。您可以使用与展开关键帧动画完全相同的方式创建这些动画,只需将起始值和结束值对调即可。
const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;
更高级的版本:圆形揭露
您还可以使用此方法制作循环展开和收起动画。
其原理与上一个版本大致相同,对一个元素进行缩放,并对直接子元素进行计数器缩放。在本例中,放大后的元素的 border-radius
为 50%,使其呈圆形,并由具有 overflow: hidden
的另一个元素封装,这意味着您不会看到圆形超出元素边界。
关于此特定变体,需要提醒一下:由于文本的放大和反向放大导致舍入误差,因此在低 DPI 屏幕上,Chrome 在动画期间会出现模糊的文本。如果您有兴趣了解相关详情,可以为您举报的 bug 加注星标并关注。
圆形展开效果的代码可以在 GitHub 代码库中找到。
总结
至此,您已经了解了使用缩放转换实现高性能剪辑动画的方法。在理想情况下,如果剪辑动画能够加速(存在由 Jake Archibald 提出的 Chromium bug),那就太好了,但在我们实现这一目标之前,为 clip
或 clip-path
添加动画效果时应格外小心,绝对避免为 width
或 height
添加动画效果。
使用 Web Animations 来实现此类效果也很方便,因为它们具有 JavaScript API,但如果您仅为 transform
和 opacity
设置动画,则可以在合成器线程上运行。很遗憾,对 Web 动画的支持不太好,但您可以使用渐进式增强功能来使用 Web 动画(如果有)。
if ('animate' in HTMLElement.prototype) {
// Animate with Web Animations.
} else {
// Fall back to generated CSS Animations or JS.
}
在此之前,虽然您可以使用基于 JavaScript 的库来实现动画,但您可能会发现,通过预编译 CSS 动画并改为使用该动画,可以获得更可靠的性能。同样,如果您的应用已经依靠 JavaScript 实现动画,那么至少与现有代码库保持一致可能会获得更好的效果。
如果您想查看此效果的代码,请查看 GitHub 上的界面元素示例代码库。一如既往,欢迎在下方的评论区告诉我们您的进展。