结一

2 年前
  • 13134

    浏览
  • 2

    评论
  • 8

    收藏

搞定这些疑难杂症,向css3动画说yes

本文作者:IMWeb 结一 原文出处:IMWeb社区 未经同意,禁止转载

本文篇幅比较长,涉及到的知识点也比较多,如3d,动画性能,动画js事件等,参考文献及demo展示也比较多,所以建议pc阅读效果更佳。

动画库

到现在来说css3动画也不是什么新技术,既然是要搞定它,好歹我们也得先看下别人做的一些东西吧,所以在此先向各位推荐几个比较好用的动画库:

关于css3动画不得不说的几个属性

看完上面那些动画库,心痒就不如行动了。

说起css3动画,有一个属性我们绝对避不开了,那就是transform这个属性,而如果要搞点高级的3d特效,那还有两个比较容易混淆的东西perspectivepreserve-3d,下面我们简单说明关于这些的一些疑难点。

transform

1、任何非none值的transform会导致创建一个堆栈上下文和包含块。

所以如果父级元素设置了transform属性,position:relative/absolute/fixed会基于此定位,详细请参考:transformed element creates a containing block for all its positioned descendants

demo如下:

.demo{
  width: 200px;
  height: 200px;
  transform: translate(100px,100px);
  background: #f00;
}
.child{
  width: 100px;
  height: 100px;
  background: #000;
  position: fixed;
  top: 50px;
  left: 20px;
}

2、transform属性值覆盖问题

我们知道transform可以有四个不同的变换,分别为scaletranslateskewrotate。现在的问题是当有两个transform设置不同变换时,权重大的覆盖权重小的。

.demo{
    transform: scale(2);
}
.demo{
    // 第一条scale将会被覆盖,失效
    transform: translateX(50px); 
}

// 如果要包含第一条scale
.demo{
    transform: scale(2) translateX(50px);
}

这个问题在平时使用中还好,但是在动画中那就相当麻烦了,因为你必须还得去拷贝之前设置的值。所以水平垂直居中的弹窗如果用了translate水平定位,然后再使用transform动画,那就毁了。

注: 听说谷歌正在拆分这四个值,这样就简单多了。

3、transform几个值的先后问题

对不起,那四个值的真的不是随便写的,它是有先后的。第一会改变中心点,第二会改变坐标系,所以请遵循先后顺序。以立方体为例:

cube demo

<section class="container">
  <div id="cube">
    <figure class="front">1</figure>
    <figure class="back">2</figure>
    <figure class="right">3</figure>
    <figure class="left">4</figure>
    <figure class="top">5</figure>
    <figure class="bottom">6</figure>
  </div>
</section>
.front  { transform: rotateY(   0deg ) translateZ( 100px ); }
.back   { transform: rotateX( 180deg ) translateZ( 100px ); }
.right  { transform: rotateY(  90deg ) translateZ( 100px ); }
.left   { transform: rotateY( -90deg ) translateZ( 100px ); }
.top    { transform: rotateX(  90deg ) translateZ( 100px ); }
.bottom { transform: rotateX( -90deg ) translateZ( 100px ); }

可以看到在设置六个面的时候,我们是先旋转(改变了坐标系),然后通过translateZ定位即可。这就跟我们军训的时候站军姿一样,每转动一次,我们的坐标随即改变,如向左转,转完之后就不再是左而是我们的正前方了。

perspective

perspective属性指定了观察者与z=0平面的距离,使具有三维位置变换的元素产生透视效果。z大于0的时候三维元素比正常大,而z小于0时则比正常小,大小程度由该属性的值决定。

该属性不可继承。由于不可继承,所以只对一级子元素管用。

原理如下图(d表示perspective的值,Z表示translateZ的值):

perspective distance

当然观察者站在哪里也会影响效果,如下图改变perspective-origin

perspective distance origin

transform-style

transform-style属性指定了,该元素的子元素是(看起来)位于三维空间内,还是在该元素所在的平面内被扁平化。如值为preserve-3d则创建一个3D渲染上下文,其直接子元素有一个共同的三维坐标系。

同样该属性不可继承,只应用于直接子元素。

perspective 与 preserve-3d 差别

前面的概念性解释太过笼统,好像都跟3d有关, 但是区别呢?区别呢?

简单来说,perspective为其直接的具有三维变换的子元素产生一个透视效果;而preserve-3d则为其直接的子元素提供一个3d渲染空间。

具体效果,请查看 perspective和transform-style 的 demo演示

最佳实践

相信经过上的一些列demo演示,对这两个总有所区分了解了。如果你还不太明白的话,那就记住这个最佳实践吧。外面一层提供透视,然后再包裹一层提供3d渲染空间。

.perspective
    .preserve-3d
        .child*n

动画

上面说了那么多疑难点,算是为我们的动画铺平了一些道路,现在正式进入我们的动画。

transition

transition: [property] [duration] [timing-function] [delay];

1、不可自动触发,可以通过改变class,改变状态(:hover, :active, :checked等)触发

2、display的none与其他值的切换不行, 通过delay设置也不行,除非通过回调函数或setTimeout先切换display,再设置动画样式改变。

.demo{
    display: none;
    width: 100px;
    transition: width .3s; // 在none和其他值之间切换,动画无效
}
.demo:hover{
    display: block;
    width: 200px;
}

3、可设置delay为负值,表示动画已经运行到了该时间,前面的动画效果忽略,见demo

4、可对自己或子元素进行动画动画: combined transitions 1

5、或对同级下面的元素及其子元素进行动画控制: combined transitions 2

6、可在状态内添加transition,覆盖默认的transition,见demo

// 鼠标滑过会采用hover的transition即1s(覆盖了默认的transition),滑出会采用默认的transition即5s
.demo{
  width: 200px;
  height: 200px;
  background: #f00;
  transition: all 5s;
}
.demo:hover{
  transition: all 1s;
  background: #000;
}

animation

语法:

animation: [[@keyframes](/user/keyframes) name] [duration] [timing-function] [delay] 
[iteration-count] [direction] [fill-mode] [play-state];


animation: slidein 3s ease-in 1s 2 reverse both paused;

1、可自动触发,也可以通过状态或增加class触发

2、安卓低端机不支持伪元素(::before::after)动画

3、animation-fill-mode 可设置动画结束及开始的状态。如为backwards,则元素默认应用第一关键帧的样式,忽略delay,可通过一开始就暂停观察(animation-play-state: paused;);如为forwards,则在动画结束后,元素将应用动画结束后的属性值;如果animation-fill-mode的值为both,则动画会遵循backwards和forwards的规则。也就是说,它会设置开始和结束的状态。

4、animation-timing-function默认应用在每个关键帧之间的变化,而不是开始到结束整个流程。所以@keyframes 中的每个关键帧可以重新定义animation-timing-function

[@keyframes](/user/keyframes) square { 
   50% {
      top: 200px;
      left: 400px;
      animation-timing-function: ease-in-out;
   }
}

5、可以用于none到block的动画切换

查看demo,主要代码如下:

// child一开始为none,demo hover的时候使用动画显示
.demo .child{
    display: none;
}
.demo:hover .child{
    display: block;
    animation: showChild .3s both;
}
[@keyframes](/user/keyframes) showChild {
    from{
        opacity: 0;
    }
}

6、对于关键帧的安排是门技术活,所以这里推荐使用CSS3动画帧数计算器

动画js事件

1、transition动画只有一个transitionend事件,而webkit现在既支持webkitTransitionEnd,也支持标准的transitionend事件,所以只能绑定一个,不然会触发两次事件,见demo

2、如有多个属性参与动画,就会出现多个transitionend事件(这个事件标准还是有不少bug的),所以请使用jquery的one事件,或者绑定事件调用函数中随即取消绑定事件

3、Detect the End of CSS Animations and Transitions with JavaScript

function whichTransitionEvent(){
  var t,
      el = document.createElement("div");

  var transitions = {
    "transition"      : "transitionend",
    "MozTransition"   : "transitionend",
    "WebkitTransition": "webkitTransitionEnd"
  }

  for (t in transitions){
    if (el.style[t] !== undefined){
      return transitions[t];
    }
  }
}

var transitionEvent = whichTransitionEvent();
// jquery 调用
$(".button").click(function(){
  $(this).addClass("animate");
  $(this).one(transitionEvent,
              function(event) {
    // Do something when the transition ends
  });
});

// 原生js调用
var button = document.querySelector(".button"),
    transitionEvent = whichTransitionEvent();

button.addEventListener("click", function() {
  if (button.classList) {
    button.classList.add("animate");
  } else {
    button.className += " " + "animate";
  }
  button.addEventListener(transitionEvent, customFunction);
});

function customFunction(event) {
  button.removeEventListener(transitionEvent, customFunction);

  // Do something when the transition ends
}

4、animation动画有三个js事件,分别为animationstartanimationiterationanimationend

不支持的动画属性切换

  • background-image
  • float
  • height/width/top/right/bottom/left 等auto值向具体值的变换
  • display 在none和其他值之间切换
  • position 在static和absolute之间切换

timing-function

1、cubic-bezier(x1, y1, x2, y2),标准四个数字的取值范围为[0,1],但是各个浏览器的范围有所不同,chrome浏览器就允许弹性效果,如cubic-bezier(.62,-0.32,.29,1.46)

在线Cubic Bezier编辑

2、 steps()

步骤动画,规定几步完成,每步完成时间(t)为总时间(T)/步骤(n),分为start和end两种。start表示每步完成时间的开始就已经运动了,end表示每步完成时间结束才运动。

steps-start-end

demo见Twitter's "fave" animation

性能

各个属性的性能消耗

既然有这么多属性都可以运动,那么是不是所有的属性运动都一样消耗性能呢?下面我们大概看下css的渲染过程:

devtools-waterfall

如上图:css最终表现大概分成主要的四步: recalculate style(查找计算样式),layout(排布),paint(绘制),composite layers(组合层级)

而我们经常说的重排和重绘,就是发生在重新layout和重新paint,从这张图上就可以清楚的看出为什么重排比重绘更耗性能,因为重排发在在重绘的前一步,它必然会导致下一步的重绘。

layout 相关属性

- -
width height
padding margin
display border-width
border top
position font-size
float text-align
overflow-y font-weight
overflow left
font-family line-height
vertical-align right
clear white-space
bottom min-height

paint 相关属性

- -
color border-style
visibility background
text-decoration background-image
background-position background-repeat
outline-color outline
outline-style border-radius
outline-width box-shadow
background-size

composite layers 相关属性

  • transform
  • opacity

总结

不是所有的属性动画消耗的性能都一样,消耗最低的是transform和opacity两个属性,其次是paint相关属性。各属性trigger参考见css triggers

这也就是为什么我们推荐使用transform的translate带替代margin或position定位的top/right/bottom/left值了,而用transform的scaleX/Y替代width/height的运动。

3D

开启3d加速

transform: translate3d(0, 0, 0);
transform: translateZ(0);

will-change

提前告知浏览器我这里将会进行一些变动,请分配资源。

1、四大核心建议:

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。

  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。

  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。

  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

2、错误用法,直接应用在hover,没有提前告知浏览器分配资源

.element:hover {
    will-change: transform;
    transition: transform 2s;
    transform: rotate(30deg) scale(1.5);
}

3、正确应用,在进入父元素的时候就告诉浏览器分配资源

.element {
    transition: opacity .3s linear;
}
/* declare changes on the element when the mouse enters / hovers its ancestor */
.ancestor:hover .element {
    will-change: opacity;
}
/* apply change when element is hovered */
.element:hover {
    opacity: .5;
}

4、在应用变化之后,取消will-change的资源分配

var el = document.getElementById('demo');
el.addEventListener('animationEnd', removeHint);

function removeHint() {
    this.style.willChange = 'auto';
}

其他动画

参考资料

2条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。