使用 CSS 过渡效果处理自动高度

Avatar of Brandon Smith
Brandon Smith 发布

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费额度!

我们都遇到过这种情况。 您有一个元素,希望能够使用 CSS 过渡效果使其平滑地折叠和展开,但其展开的大小需要根据内容确定。 您已设置了 transition: height 0.2s ease-out。 您创建了一个 collapsed CSS 类,它应用了 height: 0。 您尝试了一下,结果……高度没有过渡。 它在两个尺寸之间切换,就好像从未设置过 transition 一样。 经过一番摸索,您发现此问题仅在高度最初或最终为 auto 时才会发生。 百分比、像素值、任何绝对单位都按预期工作。 但所有这些都需要预先硬编码特定高度,而不是允许它根据元素内容的大小自然确定。

Nikita Vasilyev 对此进行了很好的记录

在本文中,为了简单起见,我主要以 height 的形式进行描述,但此处的所有内容也适用于 width

如果您希望我有一个针对此问题的魔术般、完整的解决方案,我很抱歉让您失望了。 没有一种解决方案能够在没有缺点的情况下实现所需的效果。 但是,有多种变通方法,每种方法都有不同的优缺点,并且在大多数用例中,至少有一种方法能够以可接受的方式完成工作。 我将概述主要的方法,并列出它们的优缺点,以便您能够根据自己的情况选择最佳的方法。

为什么浏览器级别没有解决此问题?

根据 Mozilla 开发者网络文档,CSS 过渡规范有意排除了 auto 值。 似乎 一些人 已经提出了请求,但是如果您仔细考虑一下,它至少在一定程度上是有道理的。 基于内容及其与其他元素的交互方式重新计算所有元素的大小和位置的浏览器进程(称为“回流”)代价很高。 如果您要将元素过渡到 heightauto,则浏览器必须对动画的每个阶段执行回流,以确定所有其他元素应该如何移动。 这无法以简单的方式进行缓存或计算,因为它在过渡发生之前不知道起始和/或结束值。 这将大大增加后台需要执行的计算量,并可能以开发人员可能没有意识到的方式降低性能。

方法 1:max-height

如果您在网上搜索此问题,max-height 方法可能会在最初的五到十个结果中被提及。 实际上,它并不是很理想,但我认为为了进行比较,在这里包含它是有价值的。

它的工作原理如下:CSS 值只能过渡到和来自固定单位值。 但假设我们有一个元素,其 height 设置为 auto,但其 max-height 设置为固定值;例如,1000px。 我们无法过渡 height,但我们可以过渡 max-height,因为它具有显式值。 在任何给定时刻,元素的实际高度将是 heightmax-height 的最小值。 因此,只要 max-height 的值大于 auto 的结果,我们就可以简单地过渡 max-height 并实现所需效果的一个版本。

此方法有两个关键缺点

一个很明显,一个比较微妙。 明显的缺点是我们仍然必须为元素硬编码最大高度,即使我们不必硬编码高度本身。 根据您的情况,也许您可以保证您不需要超过那个高度。 但如果不是,这是一个很大的折衷。 第二个,不那么明显的缺点是,除非内容高度恰好与 max-height 相同,否则过渡长度实际上不会是您指定的长度。 例如,假设您的内容高 600px,并且您的 max-height 正在从 0px 过渡到 1000px,持续时间为 1 秒。 元素需要多长时间才能达到 600px? 0.6 秒!max-height 将继续过渡,但一旦达到其内容的末尾,真实高度将停止变化。 如果您的过渡使用非线性计时函数,情况会更加明显。 如果过渡在开始时很快,在结束时很慢,则您的部分将快速展开并缓慢折叠。 并不理想。 不过,过渡是相对主观的,因此在其他情况下此方法合适的情况下,它可能是一个可以接受的折衷方案。

方法 2:transform: scaleY()

如果您不熟悉 transform 属性,它允许您将 GPU 驱动的转换(平移、缩放、旋转等)应用于元素。 务必注意这些转换的一些特性。

  1. 它们将元素的可视表示视为图像,而不是 DOM 元素。 这意味着,例如,如果元素缩放得太远,它会看起来像素化,因为它的 DOM 最初渲染到比它现在跨越的像素更少的像素上。
  2. 它们**不会触发回流**。 同样,转换不知道也不关心元素的 DOM 结构,只关心浏览器绘制的“图片”。 这就是此方法有效的原因及其最大缺点。

实现方法如下:我们为元素的 transform 属性设置 transition,然后在 transform: scaleY(1)transform: scaleY(0) 之间切换。 它们分别表示“以元素开始时的相同比例(在 y 轴上)呈现此元素”和“以 0 的比例(在 y 轴上)呈现此元素”。 在这两种状态之间进行过渡将巧妙地将元素“压缩”到和从其自然的内容大小。 作为奖励,内部的字母和/或图像也会视觉上“压缩”自身,而不是滑出元素的边界。 缺点?由于没有触发回流,因此此元素周围的元素将完全不受影响。 它们既不会移动也不会调整大小以填充空隙。

此方法的优缺点非常明显

它要么非常适合您的用例,要么完全不适用。

这主要取决于文档流中是否有任何元素跟随所讨论的元素。 例如,像模态框或工具提示一样浮动在主文档上的内容将以这种方式完美工作。 对于文档底部的元素,它也可以工作。 但不幸的是,在许多情况下,这一个行不通。

方法 3:JavaScript

在 CSS 中管理 CSS 过渡效果将是理想的,但正如我们所了解的,有时它根本不可能实现。

如果您绝对需要具有平滑折叠部分,其展开大小完全由其内容驱动,并且页面上的其他元素将在它们过渡时围绕它们流动,则可以使用一些 JavaScript 来实现。

基本策略是手动执行浏览器拒绝执行的操作:计算元素内容的完整大小,然后将元素的 CSS 过渡到该显式像素大小。

让我们稍微分解一下。 首先要注意的是,我们使用 data-collapsed 属性跟踪部分当前是否已折叠。 这对于知道每次切换其展开时要对元素“做什么”是必要的。 如果这是一个 React 或 Angular 应用程序,这将是一个状态变量。

接下来可能会引起注意的是使用了requestAnimationFrame()。它允许你在浏览器下次重新渲染时运行一个回调函数。在本例中,我们使用它来等待,直到我们刚刚设置的样式生效。这在我们更改元素的高度从auto到等效的显式像素值时非常重要,因为我们不想在那里等待过渡。因此,我们必须清除transition的值,然后设置height,然后再恢复transition。如果这些是在代码中的顺序行,结果就像它们都被同时设置了一样,因为浏览器不会并行于 JavaScript 执行进行重新渲染(至少,就我们的目的而言)。

另一个特性是在扩展完成发生后,我们将height恢复为auto。我们使用transitionend注册一个事件监听器,它在 CSS 过渡结束时触发。在该事件监听器内部,我们将其删除(因为我们只想让它响应紧随其后的过渡),然后从内联样式中删除height。这样,元素的大小就恢复为页面正常样式定义的大小。我们不想假设它应该保持相同的像素大小,甚至不应该保持auto大小。我们希望我们的 JavaScript 执行过渡,然后让路,并且不要过度干预。

其余部分相当简单。而且,正如你所看到的,这完全达到了预期的效果。也就是说,尽管尽了最大努力,但这种方法还是有很多方面会导致我们的代码变得脆弱且容易出错。

  • 我们添加了 27 行代码,而不是 3 行。
  • 对我们 section 元素中的paddingborder-box等内容的更改可能需要更改此代码。
  • section 上的 CSS 过渡,如果恰好在高度过渡仍在进行时结束,可能会导致高度无法恢复到其默认值。
  • 禁用一帧的transition可能会破坏该元素上同时发生的其它过渡。
  • 如果某个错误导致元素的height样式与其data-collapsed属性不同步,其行为可能会出现问题。

最重要的是,我们编写的代码是过程式的而不是声明式的,这本身就使其更容易出错且更复杂。尽管如此,有时我们的代码只需要做它需要做的事情,如果它值得权衡,那么它就值得权衡。

额外技巧:Flexbox

我称这种技巧为额外技巧,因为它在技术上并没有实现预期的行为。它提供了一种确定元素大小的替代方法,在许多情况下,它可能是合理的替代方案,并且完全支持过渡。

如果你还不熟悉 Flexbox,你可能想阅读关于Flexboxflex-grow的文章。

Flexbox 是一个功能非常强大的系统,用于管理界面大小如何适应不同的情况。关于这一点已经写了很多文章,我不会详细介绍。我要介绍的是,鲜为人知的是,flex属性以及与之相关的其它属性完全支持过渡!

这意味着,如果你的用例允许你使用 Flexbox 而不是内容大小来确定大小,那么让一个 section 平滑地折叠就变得非常简单,只需要设置transition: flex 0.3s ease-out并切换flex: 0即可。虽然不如基于内容的方式好,但比来回切换像素大小更加灵活(我知道,我知道,对不起)。