CSS3 过渡效果(transition)为网页世界带来了简单、易用的动画效果,但简单的外表之下还隐藏着一些不能忽视的细节。

本文的全部内容均翻译自http://blog.alexmaccaw.com/css-transitions。如果您对其中的任何技术细节存有疑问,请以原文为准。

浏览器支持

浏览器支持是个老生常谈的问题。对于过渡效果,现代浏览器已经支持的相当好了,除了IE9及之前版本,其他的主流浏览器,甚至移动平台的浏览器都已经完全支持了这一特性。当然这只是一个概述,其详细情况可以在caniuse上查询。

使用过渡效果

使用CSS伪类,比如:hover可以简单地调用过渡效果。注意,这里我们显式地声明了过渡属性、过渡时长以及默认的过渡函数——线性函数(linear)。(demo

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

.element:hover {
  height: 200px;
}

一旦:hover激活,高度即会线性地从100px过渡至200px,耗时2秒。

transition-duration(时长)实际上是transition唯一必须的参数,而其他的值浏览器都是有默认的。transition-timing-function(过渡函数)默认为easetransition-property(过渡属性)则默认为all,表示所有可过渡属性。

但我们不想仅仅在伪类上用过渡,那样一点都不灵活。所以通常会用JS来动态增删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);
}

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

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

上面的代码使用了两种不同的过渡,元素被赋予active时上滑,而被赋予inactive时则淡出。JS所做的就是为元素动态地增加类。

过渡与渐变

并不是每个CSS属性都可以使用过渡效果。总的来说,过渡只能发生在数值之间。比如height无法从0px过渡到auto,因为浏览器无法处理那些非数值的状态变化,这样结果就是它只是立即变化。Oli Studholme整理了一份清单,详细列出了过渡所支持的属性。

另一个无法过渡的是带有渐变的background属性(虽然纯颜色的是可以的)。这不是因为什么技术限制,只是浏览器厂商目前还没有去实现。

目前针对这一情况有几个暂时性的补救措施。第一个措施是在背景渐变中引入透明度,然后指定过渡属性为颜色。比如如下(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。这是第二个措施。否则你就只能拆成两个元素,然后把它们叠起来,通过透明度凑合一下(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会捕捉指针事件,诸如:before:after这样的伪元素因此可以在这里发挥作用,而且目前Webkit核心的浏览器和Firefox都完全支持伪元素的过渡了。

硬件加速

过渡leftmargin这样的属性会导致浏览器在每一帧中都重新计算排版。这样通常会大量消耗CPU,并且可能会导致不必要的重绘,特别是当屏幕上有大量元素的时候。而在手机等电力宝贵的设备上,这个问题会更加突出。

解决方法是借助CSS的transform来调用GPU渲染CSS。简单来说,就是在过渡中把元素当作图片来处理,从而避免浏览器对排版重新计算。这将使性能得到极大的提升。强制浏览器使用GPU渲染很简单,只要使用translate3d就行了:

transform: translate3d(0,0,0);

酷!但这并不是性能问题的万能药,相反,这个措施本身还有很多副作用。因此当前仅当硬件加速显得很必要的时候再用这招。

举例来说,硬件加速会导致字体有一些微变——文字会变得细一些。这是因为硬件加速并不支持亚像素(Subpixel)的抗锯齿处理。两种渲染模式下的区别:

inline_maccman_24418277747802_raw

一个简易的措施是完全禁用亚像素抗锯齿。但这个措施本身也还是有副作用的。

font-smoothing: antialiased;

另外,不同的浏览器使用不同的硬件加速库,因此会导致跨浏览器问题。比如尽管Chrome和Safari都基于Webkit,但Chrome使用Skia来进行图像渲染,而Safari则使用CoreGraphics。两者虽然差距很小,但确实存在。

你可以用Chrome带的Inspector显示所有的重绘。打开Inspector的选项,还可以显示出高精度的重绘情况。有必要的话,再在about:flags里面打开“Composited Render Layer Borders”,可以看到GPU渲染的具体内容。关键在于通过批量更新DOM来减少重绘,尽可能地将工作交给GPU。

inline_maccman_24424148470284_raw

如果你真的遇到了硬件加速带来的显示差异,不要在嵌套的元素上使用transform3d(),或者干脆只针对特定浏览器启用硬件加速。

值得注意的是,translate3d相关的hack将会变得越来越微不足道。Chrome目前已经开始在透明度计算和2D过渡中使用GPU;iOS6 Safari则直接没法用了,需要其他的补救措施

切片

要利用GPU渲染,就必须通过使用transform换掉其他属性——比如width,以此减少排版的重新计算。但如果你真的需要长度的变化的动画呢?答案是切片。

在下面的例子中,搜索框有两个过渡状态以及两个元素。当元素处于展开状态时,两个元素会都显示出来;而在紧缩状态时,切片被第一个元素掩盖起来了。

inline_maccman_24418694845836_raw

要过渡到展开状态,我们原本要过渡width属性,但现在只需要对第一个元素进行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);
}

现在,我们无需再逐帧计算元素的宽度,整个过渡过程既柔顺又高效。

过渡函数

到现在为止,我们已经看到了不少浏览器自带的过渡函数:lineareaseease-inease-outease-in-out。但如果需要更复杂的过渡函数,则需要我们自己构造贝塞尔曲线了。

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

当然,一般做这样的曲线不靠瞎猜,而是靠现有的函数库或者图形工具来生成。

inline_maccman_24418729821816_raw

如果设值超出范围,你会得到一个富有弹性的过渡,比如:

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

编程实现过渡

用CSS来做过渡非常容易,但有时需要灵活的把握过渡,尤其是需要将多个过渡串联。幸好我们可以用JavaScript来编写过渡。

CSS过渡神奇就神奇在它可以过渡所有可能的属性,使得属性的变化似乎总是有过渡效果的。我们看看如果编程实现会是怎样(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

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

回调函数

想要串联过渡的话,我们下一步就是加上回调函数。在Webkit中,你可以监听webkitTransitionEnd事件;而在其他浏览器中,则需要自己摸索一下了。

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

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

注意,有时这个事件根本不会触发,这是因为属性值没有发生变化或没有绘制行为发生。要确保每次回调都会被调用,我们增加一个定时器即可

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

现在我们可以在设置CSS前就引调$.fn.emulateTransitionEnd(),确保过渡之后一定会有回调(demo):

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

串联过渡

一旦我们有了回调,就可以顺利地将多个过渡串联起来了。我们可以自己写一个队列来搞定,但既然已经用上了jQuery,就用一下jQuery的相关函数吧。

jQuery提供了两个函数用于队列性的回调,一个是$.fn.queue(callback),一个是$.fn.dequeue()。前者用于增加一个回调入列,而后者则用于调用回调之后的出列。

换句话说,我们要把我们的CSS过渡放到$.fn.queue callback当中,并确保它们在过渡结束时引调$.fn.dequeue。(demo

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

上面的例子非常简单,现在我们来搞点复杂的:

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

重绘

通常,当我们使用过渡时,会用到两套CSS属性,一套用于初始状态,另一套则用于过渡之后的终止状态。

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

然而,如果你这两套CSS安排的太接近,浏览器就会尝试优化属性的变化,直接无视你的起始态,也不会再有过渡效果。浏览器通常是把几个属性的过渡结合在一起之后重绘,以加速渲染,但偶尔也会因此南辕北辙。

为了避免这一情况,我们需要强制浏览器对两套CSS都进行绘制。一个简单的方法是调用DOM元素的offsetHeight值(demo):

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

绝大多数浏览器上这招都会奏效,但在Andorid默认浏览器上偶尔还是会失效。这时就只能使用定时器或者增加class了。

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

将来的发展

过渡的标准仍在不断完善。新的提案包括一个新的JS API,专门用于补充现有过渡方案的不足之处,为开发者提供更多灵活性。

实际上在Github上我们可以找到一些关于新API的补丁,它引入了一个Animation构造函数,可以传递元素及需要变化的属性等等。

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

借助这个API,你可以以同步的形式来编写动画,并提供额外的过渡函数并获得精确的回调。这实在是太棒了!

过渡啊过渡

现在,你应该对CSS过渡有了一个更深的了解,并且知道了如何通过一些简单的API来构建复杂的过渡效果。

大部分例子都已经包含在GFX当中了,除此之外还有一些附加效果,诸如slide in/out、explode in/out以及3d flipping等。

About liuyanghejerry

富有激情的前端工程师,专注GUI开发。

One Thought on “CSS3 Transition 的背后

  1. Pingback: 關於 css3 Animations & Transitions 控制的二三事 | Duncan 's blog

Post Navigation