cherryvenus

关于CSS Transition,你需要知道的事

cherryvenus · 2016-12-01翻译 · 1562阅读 原文链接 十年踪迹审校

CSS3的过渡(transition),给web应用带来了简单优雅的动画,但在她的“美丽”外表下,隐藏着许多细节。

在这篇文章中,我将会专研CSS3的过渡(transition)中更加复杂的部分,从链式和事件到硬件加速和动画函数。

让浏览器控制动画序列,通过改变帧率,减少绘画和减少GPU的工作,能够优化性能和效率。

浏览器支持

Firefox、Safari和Chrome的所有版本都支持 CSS transitions ,IE10以上也支持它。如果某个浏览器不支持CSS动画,那么属性改变就会立即生效,从而优雅降级。

Webkit内核的浏览器(Safari 和 Chrome),依然需要-webkit前缀,用于动画和渐变,但是这些很快就会被移除(chrome 26+、Safari 6.1+以上已经不需要了,具体参考这里——译者注)。

使用 transition

使用transition最简单的方式就是和CSS伪元素一起用,比如:hover。注意我们指定了属性名字,transition的时长,以及默计时函数,linear [demo]。

.element {
  height: 100px;
  transition: height 2s linear;
}

.element:hover {
  height: 200px;
}

:hover伪元素被激活的时候,这高度会动态地在两秒内从100px过度到200px

duration是唯一在transition简写中不能缺省的项。如果不提供别的属性,浏览器默认的定时方法是ease,默认影响的属性是all

当谈论到激活transition,我们不希望只限于使用伪元素 —— 因为这很显然不灵活。解决这个的方法就是用程序添加和删除class[demo].

/* CSS */
.element {
  opacity: 0.0;
  transform: scale(0.95) translate3d(0,100%,0);
  transition: transform 400ms ease, opacity 400ms ease;
}

.element.active {
  opacity: 1.0;
  transform: scale(1.0) translate3d(0,0,0);
}

.element.inactive {
  opacity: 0.0;
  transform: scale(1) translate3d(0,0,0);
}

// JS with jQuery
var active = function(){
  $('.element').removeClass('inactive').addClass('active');
};

var inactive = function(){
  $('.element').removeClass('active').addClass('inactive');
};

以上列子,我们用了2个不同的 transition,当激活的时候,元素向上滑动,当无效化之后,淡出。Javascript所要做的事仅仅是切换activeinactive这两个class。

渐变背景的 transition

不是所有的CSS属性都能 transition,最基本的规则是你只能通过绝对数值来 transition。比如,你不能让height0px过渡到auto,浏览器不能计算中间过度值,因此属性变化是瞬间的。Oli Studholme提供了便利的 一份完整的支持 transition 的属性列表

另一个重要的不能被 transition 的属性是渐变背景(虽然渐变背景可以是纯色是)。这一限制不是什么技术原因,只是因为浏览器实现对它的支持还需要一段时间。

同时,有一些很好的折衷办法。第一个办法是给渐变背景添加一个透明度,然后对背景色进行 transition。比如[demo]:

.panel {
  background-color: #000;
  background-image: linear-gradient(rgba(255, 255, 0, 0.4), #FAFAFA);
  transition: background-color 400ms ease;
}

.panel:hover {
  background-color: #DDD;
}

如果渐变是连续的,你可以对background-position;进行transition。就像这里个例子,否则,你还有最后一招,那就是创建两个元素,将一个盖在另一个之上,然后对它们的透明度进行transition[demo]。

.element {  
  width: 100px;  
  height: 100px;  
  position: relative;
  background: linear-gradient(#C7D3DC,#5B798E);    
}  

.element .inner { 
  content: '';
  position: absolute;
  left: 0; top: 0; right: 0; bottom: 0;
  background: linear-gradient(#DDD, #FAFAFA);          
  opacity: 0;
  transition: opacity 1s linear;
}

.element:hover .inner {
  opacity: 1;
}

使用最后一个方法需要注意的是,这需要额外的标记,并且确保内部的div能够捕捉到pointer事件。对 transition 来说,伪元素,类似:before:after是 transition 理想的使用场景,但不幸的是,只有 Firefox 支持伪元素的 transition。 Eliott Sprehn正在尝试让 webkit 支持,相信用不了多久就可以了。

硬件加速

某些属性的 transition,比如leftmargin会导致浏览器每帧都重新计算样式。这样的性能开销相当大,并可能会导致不必要的重绘,尤其是如果你在屏幕上有很多元素,更容易产生不必要的开销。这一问题在低功耗设备上比如手机上显得特别明显。

解决方案是使用CSS 变换(transformation)来减少渲染给GPU带来的压力。简单来说,就是在做 transition 的时候,将元素作为一张图片,以避免样式重新计算,这极大程度上提升了性能。一个简单的强迫浏览器用硬件加速渲染一个元素的方法是,通过translate3d 启用对Z轴的变换:

transform: translate3d(0,0,0);

然而这治标不治本,并且会带来许多本身的问题。只有当需要的时候,你才应该启用硬件加速,并且完全不需要在每个元素上都启用它。

比如,硬件加速会导致微妙的字体问题,比如一个字体出现的时候失去了加粗效果。这是一个bug,当元素开启硬件加速的时候,不支持亚像素抗锯齿。你可以看到在两个渲染模式下存在着清晰的差别。

antialiasing.png

下面的临时修复方案,尽管备受争议,但能够完全阻止亚像素抗锯齿。然而,注意要理解这么做的局限性

 font-smoothing: antialiased;

此外,不同浏览器用不同的硬件加速库,这可能会造成跨浏览器问题。比如,当Chrome和Safari都是WebKit内核的,但Chrome使用Skia来做图形渲染,而Safari用CoreGraphics。这两个库之间的差别是细微的,但是差别确实存在。

你可以用Chrome开发者工具概览页面,显示所有的重绘。此外你可以在开发者工具选项中选择显示绘制三角形(paint triangles),甚至可以通过 about:flags 打开复合渲染层边界,来看哪个层是作用在GPU上的。优化的关键是通过批量更新 DOM 来减少绘制,并尽可能使用GPU来渲染。

Painting

如果你在浏览器之间由于硬件加速而导致显示问题,比如闪烁或者颤动,确保你没有用transform3d()的CSS属性在元素上。作为最后手段,尝试针对不同浏览器做特定的变换。

值得注意的是translate3dhack的必要性变得越来越少。事实上,最新的Chrome发布版本能够自动使用GPU来处理透明度和2d transition。iOS6 下的 Safari已经明确禁用了这一技巧,这样我们只能用其他更多的解决方法

剪裁(Clipping)

为了充分利用GPU渲染,你需要通过使用CSS变换来避免样式的从新计算而不是直接使用如width这样的属性。若果你确实需要给元素的宽度做动画该怎么办?解决方法就是使用剪裁(Clipping)。

在以下例子中,你可以看到一个搜索框,它有两个 transition 状态。第二个展开状态被一个剪裁的元素给隐藏了。

Clipping

对这个展开宽度进行 transition,我们所需要做的就是将X轴偏移到左边。这里的关键是我们用translate3d而不是直接改变元素的宽度 [demo]。

.clipped {
  overflow: hidden;
  position: relative;
}

.clipped .clip {
  right: 0px;
  width: 45px;
  height: 45px;
  background: url(/images/clip.png) no-repeat
}

input:focus {
  -webkit-transform: translate3d(-50px, 0, 0);
}

通过确保不在每帧重新计算元素的宽度,我们让 transition 顺滑并保持高性能。

时间函数

到目前为止,我们用了一些浏览器预定义时间函数linear, ease, ease-in, ease-outease-in-out。要使用更复杂的时间函数,我们可以通过定义贝塞尔曲线的4个关键点来定义自己的时间函数,。

transition: -webkit-transform 1s cubic-bezier(.17,.67,.69,1.33);

我们不用靠猜测来确定贝塞尔曲线的这些值,可以使用预定义曲线或者依靠图形工具,这样会方便很多。

Cubic-bezier

注意,你可以将数值拖出边界范围,这样将产生一个左右摇摆的 transition,例如:

transition: all 600ms cubic‑bezier(0.175, 0.885, 0.32, 1.275);

程序化 transition

在CSS中使用 transition 非常好,但有时候你需要更多的控制权,尤其是涉及到链式过渡(chainning transitions)的时候。幸运地是我们不仅能从javascript中调用过渡,还能定义他们。

CSS transition 有一个魔法般的all属性,这确保了任何属性改变都可以被 transition。让我们看看如何实际使用它[demo]。

var defaults = {
  duration: 400,
  easing: ''
};

$.fn.transition = function (properties, options) {
  options = $.extend({}, defaults, options);
  properties['webkitTransition'] = 'all ' + options.duration + 'ms ' + options.easing;
  $(this).css(properties);
};

现在我们有了一个jQuery函数$.fn.transition,我们可以用它来程序化执行 transition。

$('.element').transition({background: 'red'});

Transition 回调

要让 transition 能够链式衔接的下一步是要能够在transition结束后执行回调。在Webkit中你可以做到,只要通过监听webkitTransitionEnd这个事件。对于其他的浏览器,你需要慢慢找到正确的事件名称。

 var callback = function () {
    // ...
  }

  $(this).one('webkitTransitionEnd', callback)
  $(this).css(properties);

注意,有时候事件会没有被触发,这常常是因为某些属性没有改变或是一个绘制没有被触发。为了确保我们总能执行回调,我们需要设置一个超时,这将会手动帮我们触发事件。

$.fn.emulateTransitionEnd = function(duration) {
  var called = false, $el = this;
  $(this).one('webkitTransitionEnd', function() { called = true; });
  var callback = function() { if (!called) $($el).trigger('webkitTransitionEnd'); };
  setTimeout(callback, duration);
};

现在我们可以在设置元素的css之前,执行 $.fn.emulateTransitionEnd(),以确保我们在 transition 动画结束之后能够执行回调。[demo]。

$(this).one('webkitTransitionEnd', callback);
$(this).emulateTransitionEnd(options.duration + 50);
$(this).css(properties);

链式 transition

现在,我们能够通过写程序来应用 transition,当它们结束之后执行回调,这样我们就能够开始写 transition 序列了。我们可以自己实现队列来做这件事,但是由于我们用了jQuery,我们也可以直接使用现成的功能。

jQuery提供了2个关键函数来与它的队列API交互,它们是$.fn.queue(callback)$.fn.dequeue()。前者负责往队列中添加一个回调,而后者负责执行当前队列中的下一项。

换句话说,我们需要$.fn.queue回调之中设置我们的CSS transition,然后确保当 transition 完成之后,调用$.fn.dequeue[demo]。

var $el = $(this);
$el.queue(function(){
  $el.one('webkitTransitionEnd', function(){
    $el.dequeue();
  });
  $el.css(properties);
});

这个例子相对简单,但是它让我们创建了复杂的链式动画,甚至使用了jQuery的delay()函数:比如:

$('.element').transition({left: '20px'})
             .delay(200)
             .transition({background: 'red'});

重新绘制

在 transition 的时候,你常常想要设置两组CSS属性。一组初始化属性,指明动画应该从哪里开始,以及一组结束属性,表示 transition 应该在哪里结束。

$('.element').css({left: '10px'})
             .transition({left: '20px'});

然而,你会发现如果你应用了这两组属性,后一个在前一个之后立即运行,那么浏览器会尝试优化属性改变,结果就无视你的初始属性并阻止了 transition。它背后的机制是,浏览器绘制之前,批量处理属性变动,这通常会加速渲染,但有时会有不良反应。

解决方法是在两组属性之间强迫重绘。一个简单的方法是获取Dom元素的offsetHeight属性,就像这样[demo]:

$.fn.redraw = function(){
  $(this).each(function(){
    var redraw = this.offsetHeight;
  });
};

这在大部分浏览器中有有效,但是我有次凑巧用在Android中,发现这依然不行。可供替代的方法有timeout或者切换class名(建议用 requestAnimationFrame,译者注)。

$('.element').css({left: '10px'})
             .redraw()
             .transition({left: '20px'});

未来

Transition 正在被开发者积极使用,而它的下一个标准看上去非常有前景。这个提案包括了一个新的javascript的API,这个api专注于解决 transition 现存的限制,并给与开发者更多的灵活性。

实际上,你可以在github上找到对新API的shim。它包括一个Animation构造函数,传递一个元素给它,让它做动画,同时传递要做动画的属性,以及其他的属性,比如延迟。

var anim = new Animation(elem, { left: '100px' }, 3);
anim.play();

有了这个新API,你可以同步各个动画,为动画提供自定义时间函数,还可以在动画结束时执行回调。这真是件激动人心的事啊!

过渡

到目前为止,我希望你对CSS transition有了更深层次的了解,并了解简单的 API 如何能结合起来提供复杂和丰富的效果。

大多数javascript例子都直接来自于GFX,它是一个jQuery版的CSS transition库。同样在core library,我包含了一些的额外的特效,比如滑动进/出,外加进/出和3D翻转。


感谢 Paul Irish 审校了这篇文章。

顺便说一句,我根据这篇文章的内容整理出了一个演讲。如果你知道或者正在组织一场会议,我的演讲可能契合你的主题,这样的话我希望你联系我!在去年一年里我做了六场会议的演讲,而我想在2013年里做得更多。
相关文章