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
(过渡函数)默认为ease
,transition-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都完全支持伪元素的过渡了。
硬件加速
过渡left
、margin
这样的属性会导致浏览器在每一帧中都重新计算排版。这样通常会大量消耗CPU,并且可能会导致不必要的重绘,特别是当屏幕上有大量元素的时候。而在手机等电力宝贵的设备上,这个问题会更加突出。
解决方法是借助CSS的transform
来调用GPU渲染CSS。简单来说,就是在过渡中把元素当作图片来处理,从而避免浏览器对排版重新计算。这将使性能得到极大的提升。强制浏览器使用GPU渲染很简单,只要使用translate3d
就行了:
transform: translate3d(0,0,0);
酷!但这并不是性能问题的万能药,相反,这个措施本身还有很多副作用。因此当前仅当硬件加速显得很必要的时候再用这招。
举例来说,硬件加速会导致字体有一些微变——文字会变得细一些。这是因为硬件加速并不支持亚像素(Subpixel)的抗锯齿处理。两种渲染模式下的区别:
一个简易的措施是完全禁用亚像素抗锯齿。但这个措施本身也还是有副作用的。
font-smoothing: antialiased;
另外,不同的浏览器使用不同的硬件加速库,因此会导致跨浏览器问题。比如尽管Chrome和Safari都基于Webkit,但Chrome使用Skia来进行图像渲染,而Safari则使用CoreGraphics。两者虽然差距很小,但确实存在。
你可以用Chrome带的Inspector显示所有的重绘。打开Inspector的选项,还可以显示出高精度的重绘情况。有必要的话,再在about:flags里面打开“Composited Render Layer Borders”,可以看到GPU渲染的具体内容。关键在于通过批量更新DOM来减少重绘,尽可能地将工作交给GPU。
如果你真的遇到了硬件加速带来的显示差异,不要在嵌套的元素上使用transform3d()
,或者干脆只针对特定浏览器启用硬件加速。
值得注意的是,translate3d
相关的hack将会变得越来越微不足道。Chrome目前已经开始在透明度计算和2D过渡中使用GPU;iOS6 Safari则直接没法用了,需要其他的补救措施。
切片
要利用GPU渲染,就必须通过使用transform
换掉其他属性——比如width
,以此减少排版的重新计算。但如果你真的需要长度的变化的动画呢?答案是切片。
在下面的例子中,搜索框有两个过渡状态以及两个元素。当元素处于展开状态时,两个元素会都显示出来;而在紧缩状态时,切片被第一个元素掩盖起来了。
要过渡到展开状态,我们原本要过渡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); }
现在,我们无需再逐帧计算元素的宽度,整个过渡过程既柔顺又高效。
过渡函数
到现在为止,我们已经看到了不少浏览器自带的过渡函数:linear
、ease
、ease-in
、ease-out
、ease-in-out
。但如果需要更复杂的过渡函数,则需要我们自己构造贝塞尔曲线了。
transition: -webkit-transform 1s cubic-bezier(.17,.67,.69,1.33);
当然,一般做这样的曲线不靠瞎猜,而是靠现有的函数库或者图形工具来生成。
如果设值超出范围,你会得到一个富有弹性的过渡,比如:
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等。
Pingback: 關於 css3 Animations & Transitions 控制的二三事 | Duncan 's blog