CSS3 transition规范的实际使用经验

407 查看

本篇文章主要讲述CSS3 transition规范和在不同浏览器之间的使用差异,关于具体解决方法或如何规避问题的意见可以参考另一篇非常有见地的文章,“All You Need to Know About CSS Transitions”。Alex MacCaw讲述的是关于实现特定的效果,而我要谈的是技术背景,主要讨论在使用CSS过渡的过程中所未预料到的问题。

结构 (HTML),表现(CSS),以及行为(JavaScript)相分离并不是什么新鲜的事情,然而 CSS 能跨越这个界限并且可以在短期内得到实际的应用,这还真的是一个完全不同的讨论话题。

几周前,我开发一个 JavaScript 模块,在能够使用 CSS 过渡的条件下,JavaScript 端又无法获取到实现过渡的方式。实际遇到的问题是这两者根本没有办法同步,经过多次的测试后,我只能放弃。而我的测试结果正是本文所讲述的。

首先,我们要说一下getcomputedstyle(),是一种用 JavaScript 返回浏览器渲染CSS的属性值的方法。 这个方法可以查看“DOM Level 2: getComputedStyle()”和“CSS Level 2: Computed Values”。

这对于像 font-size 这样的属性, 通过一个参数便可以转换为像素值。 但对于可以缩写的属性值,例如 margin ,一些浏览器则返回为空。再就是那些同一属性的不同属性值,例如 font-weight 的值 bold 和700。WebKit也有一个小bug,它会从伪对象中提取出属性值。

这里所讲述的浏览器之间的差异是2013年1月在使用 Firefox18(Gecko),Opera 12.12 (Presto), Internet Explorer10(Trident),Safari 浏览器6.0.2(WebKit),Chrome 23(WebKit) 以及 Gecko 和 WebKit的 Nightly build channels。

事不宜迟,让我们来一起看一下规范与实际情况的差异,为了方便,我省略了各浏览器的前缀。在文中我通过创建一个 CSS3 Transitions Test Suite 来发现问题。

1、指定过渡
CSS3 transitions 规范定义了以下四个 CSS 属性:

  • transition-property
  • transition-duration
  • transition-delay
  • transition-timing-function

过渡属性
transition-property 是用来指定当元素其中一个属性改变时执行 transition 效果。系统默认值是 all,这意味着浏览器能够以动画形式呈现所有的可过渡属性(transition-duration持续时间超过0s),该属性支持单个值或以逗号隔开的多个值列表(跟其他所有transition-*属性一样)。

规范规定,一个浏览器应该接受并保存任何它不能识别的属性。因此,下面的例子中将会看到持续2秒的 padding 过渡:

不同于规范的是,上面的情况在 WebKit 下会解析为 transition-property: all。 而 Firefox 和 Opera 会解析为 transition-property: all, padding.

过渡持续时间
transition-duration 属性规定了一个过渡从初始状态到目标状态的持续时间。它接受以秒或毫秒的值(例如,2.3S和2300ms都是指2.3秒)。
尽管规范明确规定了过渡值必须为正数,但 Opera 仍接受-5S的值,至少对于getComputedStyle()来说是这样的。虽然规范中并没有限制属性值的大小,但 Opera 和 IE 不接受低于10ms的值。而 WebKit 在 getComputedStyle()执行中有个小bug,例如:返回值0.009999999776482582s会取代0.01s。

过渡延迟时间
transition-delay 属性规定了在执行一个过渡之前的等待时间,同样使用值。Delay 可以是负值,但这会导致动画无法平滑过渡。
IE 和 Opera 不接受 transition-duration 在-10ms和10ms之间的值。WebKit 的 floating point 也会在这儿出现。

transition-timing-function 属性规定了过渡效果的时间曲线。包括cubic-bezier(x1, y1, x2, y2), step(, start|end),和预先定义的 cubic-bezier 曲线关键词,linear, ease, ease-in, ease-out和ease-in-out。在使用 LEA Verou 特有的 cubic-bezier 曲线编辑器时,cubic-bezier 背后的公式就变得不再重要。尽管 cubic-bezier 曲线会平滑过渡,但是step()函数会在一个固定的间隔跳到下一个值。这样便会产生逐帧动画的效果;如“Pure CSS3 Typing Animation With steps()”。

linear 的计算值通常表示为 cubic-bezier(0, 0, 1, 1)—— WebKit除外。但 WebKit 仍然会返回 cubic-bezier(0.25, 0.1, 0.25, 1),而不是 ease。规范规定 X 值的必须介于0和1之间,y 值可以超过该范围,而WebKit 允许 X 超过此范围,而 Android 浏览器(4.0版本)却混淆了x和y的范围。

2 过渡完成
我前面已经提到了 CSS 过渡异步运行的问题。规范提及了 TransitionEnd 事件允许 JavaScript 与已完成的过渡同步进行。但可恶的是该规范对此并没具体阐述。事实上,它只是简单地说明单个事件会因为已完成过渡的属性而被终止。

规范指出缩写属性(如padding)应为包括其在内的所有属性(padding-top,padding-right,等等)实现过渡,它并没有说哪个属性应该在 TransitionEnd 事件中被具体命名。然而即使过渡被定义为缩写属性(如padding),Gecko,Trident 和 Presto 对于普通书写的子属性(如padding-top)同样可以实现过渡,而 WebKit 则会阻止过渡。 如果你指定 transition-property: padding,WebKit 会为 padding 执行过渡, 但 transition-property: all 这样就会针对 padding-left 执行新的过渡。而当 padding 正执行过渡时, iPhone 6.0.1 的 Safari 浏览器在也可以执行 font-size 和 line-height的过渡。

以上 CSS 将在不同浏览器下触发不同的 TransitionEnd:
Gecko,Trident,Presto:
padding-top,padding-right,padding-bottom,padding-left
WebKit:
padding

以上 CSS 将在不同浏览器下触发不同的TransitionEnd:
Gecko,Trident,Presto,WebKit:
padding-top,padding-right,padding-bottom,padding-left
Safari 6.0.1 on iPhone:
padding-top, padding-right, padding-bottom, padding-left, font-size, line-height

你可以指定负值 transition-delay 来“快速实现”转换。但是transition-duration: 1s; transition-delay: -1s; 在 Gecko 和 WebKit 下执行转换并会立即跳转至目标值。而Trident 和 Presto 将不会触发任何事件。

WebKit在 getComputedStyle() 上遇到的浮点问题也同样存在于 TransitionEnd.elapsedTime 中,所有的浏览器如此。 Math.round(event.elapsedTime * 1000) / 1000 可辅助修复。

WebKit 和 IE 浏览器下执行 background-position,会触发对 background-position-x 和 background-position-y 的 TransitionEnd,而不是 background-position 的TransitionEnd。

所以,即使你知道过渡正在执行,你也不能依赖已有的 TransitionEnd.propertyName。尽管你可以编写大量的 JavaScript 来弥补,但在没有对每一个属性进行恰当性能检测的情况下,即使你采用最新方法也将无法实现。

3 过渡属性
规范列出了浏览器支持动画过渡的一些CSS属性。当然也包括CSS2.1的属性。还有一些可以动态变化的新属性,如 Flexible Box Layout

该属性数值类型非常重要。margin-top 接受和值,但根据可过渡CSS属性列表,只有是可实现动画效果。但这并不能让浏览器开发商避开值实现过渡。然而,word-spacing 属性除外。该属性包括值,但没有浏览器能以动画形式显示。

撇开 TransitionEnd 事件,如果在过渡发生的指定时间内,getComputedStyle()值从A变到B,该属性就会从值A过渡为值B。如果没有执行,例如“CSS属性值发生变化”,那么也许应该仔细核查下DOM。setTimeout()的解析度还不够好以达到快速过渡(小于几百毫秒的持续时间),这时候requestAnimationFrame()就是你的帮手。在重绘前会提醒你,并提供了一些中间值供参考。除了opera,其他的都可以支持。

4 过渡属性的优先级
transition-property 规范允许多次过渡单个属性,如果单个属性在“过渡属性”中的值被多次指定,过渡将通过持续时间,延迟和时间曲线给出的值来实现。因此,我们可以实现 padding 过渡持续1秒,padding-left 过渡持续2秒; 或使用 transition-property: all 来定义默认属性类型并重置特定属性。
在 Firefox 和 IE 浏览器上,这些都没有任何问题。 但 opera下会混淆优先顺序。它认为 padding-left 比padding 和 all 更加具体,而不是简单地使用最后一个属性。

最大的问题是WebKit。如果一个属性被多次指定,它将进行多次过渡。 如果想让WebKit崩溃,尝试用transition-duration :0.1秒运行transition-property: padding, padding-left,WebKit将至少执行两次过渡。但更有意思的是,TransitionEnd可以进行上百次的单一过渡。

5 auto的转变
CSS 属性中的 auto 值能够自适应宽度,如果块级元素设置了width: auto,那么就会继承父级的宽度。有时你需要从 width: auto 改变到一个具体宽度,并且需要过渡那个改变。当然本规范并没有强制或否定 auto 值可用于过渡。

Firefox,IE 和 Opera 无法从 or 值过渡到 auto 值。 IE 下有 z-index 有一点点例外,但仅此而已。 另一方面,WebKit 能够从and 过渡到几乎可以接受 auto 值的任意CSS 属性。WebKit 不太喜欢 clip;因为这个属性,它只会引发 TransitionEnd 过渡,而过渡期间不会产生或显示任何中间值或状态。
对于其他属性,如 width 和 height,WebKit 下会有一些差异。如果 width: auto 过渡为 300px 的宽度,然后再过渡成 100px,那么过渡不会从 300 缩至100 像素。它会从 0 增加到 100 像素。

关于完整的兼容性列表,可以查看“CSS Animatable Properties.”

6 隐式过渡
隐式过渡发生在当一个属性的改变引起另一个属性被过渡的时候, 或者当你想改变一个父级元素中的属性, 会导致子元素不论是继承过渡或附属属性的过渡。font-size: 18px, padding: 2em—–padding 会被计算为 2 × font-size, em 就是36像素。

有各种各样的相对值类型:, , em, rem, vh, vw等等。使用一个相对值,如 padding: 2em,让浏览器重新计算属性的 getComputedValue(),每次应变量(如font-size)都会发生改变。由于计算样式改变,将反过来导致 padding 的过渡。这种过渡被定义为“隐式过渡”,因为padding属性值没有被修改。

大多数浏览器会实现这种隐式过渡。除了 IE 10,只对 line-height 属性执行隐式过渡。除了 vertical-align 外,Webkit 可以针对其他所有属性执行隐式过渡。除了字体相对属性值,还有宽度相对属性值(通常为),相对属性值(如vh和vw),默认初始值(Opera中的column-gap: 1em),还有“currentColor”。所有这些都有可能会,也可能不会引起隐式过渡。

在 Firefox 中, 当继承和附属属性执行过渡,但他们的 transition-duration 或 transition-delay 并没有随着过渡, 这些隐式过渡就会变得特别有趣。 而 Webkit 和 Opera 执行过渡时会很有视觉感,但 Firefox 会稍显错乱。在IE中不会轻易执行隐式过渡。

另外,别忘了继承, DOM 元素的 font-size 将会由其子元素继承,只要不被覆盖,就可能引起隐式过渡。

7 转换和伪元素
伪元素(:before和:after),在 CSS2 中已经有了介绍. 如果不熟悉可以查看 “Learning to Use the :before and :after Pseudo-Elements in CSS”。 虽然 CSS3 中定义了额外的伪元素(::alternate,::outside),但是他们(到目前为止)还并没有被支持。因此所有 CSS 动画属性也应该是伪元素的动画属性。

Firefox 和 IE 10 可以在伪元素上实现属性过渡. 而 Opera,Chrome 和 Safari 则不会。 WebKit 从2013年一月起也开始支持。

伪元素的过渡会导致内容自身产生一些新问题,因为在生成内容时 TransitionEnd 过渡根本还没有结束。 在某一时间段内,他们理应在主元素上被触发,并通过 TransitionEnd.pseudoElement 提供伪元素,但即便是“CSS动画过渡”的“过渡事件”部分,编写者的方案也并没有指定哪一个最合适。

我们想要改变 content 属性值,因此IE 8将在特殊情况下(比如:hover状态)将会重新渲染该元素。结果表明,对老的IE版本进行兼容会影响到所有其他浏览器的效率。所以, 当试图在伪元素上进行属性过渡时,要确保 content 的值不会被改变。

如果主元素没有运行:hover状态,那么 IE 10 将不针对伪元素“:hover”执行过渡。

在 IE10 下,:before在 mouseover 的时候,:hover 是一定要定义的。

这个问题在于不是一定要求你定义主元素:hover 状态。而是如果没有定义,IE 10 会将:hover解释为:active。更奇怪的是,:active状态甚至会在 mouseup 后继续持续,而当你再次点击就会取消。

8 背景标签
在编辑标签时,IE 10 是唯一可对背景或前景响应的浏览器,如果标签变为背景后,虽然它会完成正在执行的过渡,但它不会执行新的过渡。IE 10 将等到标签变为前景后再执行新过渡。幸运的是,IE 10 已经支持页面的可见性 API,允许开发人员应对这种操作行为。

9 隐藏元素
对于隐藏的元素,过渡是不会被执行的,因为大多数浏览器都明确认为没有必要在一个看不见的元素里运行过渡。然而,也有特例,在 Opera 下无论元素隐藏与否它都将执行过渡。

10 过渡之前,DOM树是否加载完毕
当文档脱离解析模式时,DOMContentLoaded 被触发,如果你在使用 jquery,那么应该了解jQuery.ready(),过渡可以在这之前运行。

11 渲染差异
这个问题我之前已经说过了, 本文就是基于我的测试结果进行阐述的。测试是自动运行的,但事实证明,还是发现了很多问题。
当时要实现从渐变到渐变的背景过渡是不可能的,但可以实现从渐变到纯色的过渡。如果渐变正在进行中,从白色到目标颜色的过渡即将开始,在过渡启动时,会看到白色在快速闪动。目前所有的浏览器中都可以察觉到这一点。

不过Firefox 似乎是用不同的算法来渲染图像的,以表明它们执行了动画过渡(见实例)。很显然,在动画过渡时, Gecko 并没有呈现好的效果。如果 transform: scale() 足够低,这种情况将发生。

Firefox 不会从 a:visited 到 a:hover 过程中过渡动画,反之亦然。 但它会从 a:visited 直接跳到 a:link,然后过渡到 a:hover 状态, 你可以在这个例子中看到,这是在 Mozilla Developer Network”Privacy and the :visited Selector”中提到的。然而 IE 10 与 Chrome,Safari 和 Opera 浏览器一样,会从a:link到a:visited实现过渡。
如果子元素的 position 改变时, Firefox 不会触发元素的属性, 而 WebKit,Opera 和 IE 10 则会触发。

12 对规范的建议
看完了整个规范并对所有功能进行了测试之后,觉得如果能进行以下优化将会更好:

  • 加入TransitionsEnd(注意是复数),一个元素的所有过渡一旦完成就启动触发。它能告知一系列已被触发的属性,但是不需要知道哪些已被过渡, 只要知道所有的动画过渡何时可以完成即可。
  • 加入 TransitionStart 任务,以便可以获取每个待过渡属性。因为 JavaScript 的事件循环和渲染路径不一定能互相牵制,单一的 TransitionsStart(也会重复多次)可能是更好的解决方案。我不知道为什么要 cancel 任务,所以这就叫“操作后就不再管”。
  • 要明确哪些 TransitionEnd 需要被触发,前面举例的 WebKit 中 padding 和 padding-left 的问题会让人很头疼。
  • 要明确说明“隐形过渡”如何处理, 前面例子中 transition-property: font-size的line-height: 1em 应该要有明确的处理方式。
  • 需要添加那些允许定义 pointer-events: none 并防止意外悬停状态的::transitioning伪类,这里防止滥用样式,因为他们自身会引发新的过渡或者改变正在进行的过渡。除了这些建议,我们还需要能在不大量使用 JavaScript 进行辅助的情况下进行一些常规操作。
  • 有时候你需要禁用过渡。例如,为了在网站访问者面前呈现完美过渡之前,你需要调整布局并对尺寸规格进行精确测算对位置进行完美布局。
  • 有时你想立即从 DOM 中移除一个对象。你可以添加一个类,等待 TransitionEnd 完成后再进行删除。
  • 跟删除对象一样,你想要添加一个新元素。你可插入这个元素,设置“隐藏”以实现新元素的样式变化。
  • 重新排序,隐藏和显示元素都比较常见。针对这些进行样式操作就要像操作实用程序一样,如 Isotope

13 使用delay
使用延时,可以很好的解决无意的鼠标悬停造成的样式变化,如同setTimeout()。

14 总结(可参考前面谈到过的实例)

  • 使用 transition-property: all 时注意,否则将遇到本不需要进行转换的 TransitionEnd 情况。
  • 当使用可缩写属性时,触发事件的数量会根据不同浏览器而不同。
  • Opera 和 IE 不支持延迟时间为负值。
  • Webkit在属性优先级上存在问题,例如:要避免transition-property: margin, margin-left的情况。
  • IE不支持隐式转换。
  • Firefox和Opera无法解析 transition-property: all, width。
  • Opera 混淆了属性的优先级。
  • 伪元素的过渡不会影响 TransitionEnd。
  • 伪元素的过渡在 IE 10 下会出现:hover的bug。