最近,我很荣幸成为搭建 Google I/O 2015 网站 团队的一员,并在去年搭建了 Chrome Dev Summit 网站。这两个网站我都使用了 FLIP,本质上它是一个准则,而不是一个框架或库。这是对动画的一种思考,试图在浏览器中能更轻易地让动画达到 60 fps。
如果你更乐于观看视频,这是我在 Chrome Dev Summit 的交流分享,详细地阐述了 FLIP(标题里没有明确地说明):
视频链接:https://youtu.be/RCFQu0hK6bU
总体策略
我们尝试将动画翻转过来(翻转?天呐,太搞笑了吧)而不是直接过渡,因为这需要对每帧进行昂贵的计算。通过动态预计算动画,可以让它更轻松地完成。
FLIP 代表 First、Last、Invert、Play。
分别解释:
- First:元素参与 transtion 的初始状态。
- Last:元素的最终状态。
- Invert:这里有点意思。你弄清楚了元素从开始到结束是如何改变的,如它的 width、height、opacity。下一步,你应用
transform
和opacity
的变化来扭转或反转它们。如果元素已经向下移动到初始和结束状态之间的 90px,然后在 Y 轴上应用 transform -90px。这让元素看起来仍然在初始的位置,所以元素并没有达到最终的位置。 - Play:为你之前改变的属性开启过渡,然后移除反转的改变。因为当移除 transform 和 opacity 时,元素都在它们的最终位置。这会缓解从伪造的初始位置到最终位置的计算量。
有代码吗?
恩,当然有。在这里我会分解代码来向你解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 获得初始位置 var first = el.getBoundingClientRect(); // 设置元素的最终位置 el.classList.add('totes-at-the-end'); // 再次读取。这会强迫同步布局,所有要小心。 var last = el.getBoundingClientRect(); // 如果需要,可为其它计算得到的 style 做同样的操作。 // 尽可能使用仅触发渲染层合并的属性(如 transform 和 opacity) var invert = first.top - last.top; // 翻转. el.style.transform = 'translateY(' + invert + 'px)'; // 等待下一帧,直到我们知道所有样式的改变已准备好。 requestAnimationFrame(function() { // 启动动画 el.classList.add('animate-on-transforms'); // GO GO GOOOOOO! el.style.transform = ''; }); // 通过 transitionend 捕获结束状态 el.addEventListener('transitionend', tidyUpAnimations); |
然而,你也可以使用即将推出的 Web Animations API 实现同样的效果,而且它更容易实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 获取初始位置 var first = el.getBoundingClientRect(); // 将其移动到最终位置 el.classList.add('totes-at-the-end'); // 获取最终位置 var last = el.getBoundingClientRect(); // 翻转 var invert = first.top - last.top; // 从翻转的位置到最终位置 var player = el.animate([ { transform: 'translateY(' + invert + 'px)' }, { transform: 'translateY(0)' } ], { duration: 300, easing: 'cubic-bezier(0,0,0.32,1)', }); // 动画结束后做一些处理。 player.addEventListener('finish', tidyUpAnimations); |
此刻你需要 Web Animations API polyfill 像上面一样使用代码,而且它是轻量级的,会让开发更加轻松!
如果你想获取更多关于 FLIP 在生产环境上的应用,可到 Chrome Dev Summit 网站查看。
这样做有什么好处?
在用户与你网站交互后有 100 ms 空闲期,此时,你可以在用户不注意的情况下运行。
在你对用户输入作出响应后的这段时间,增加一些动画响应效果是非常好的。例如,在 Chrome Dev Summit 某个案例中,当用户触摸卡片时,会将其放大。一般情况下,元素开始与结束状态的位置和尺寸大小是未知的,但网站响应迅速并有元素移动。这将会对你有所帮助,因为它会精确测量元素,并在运行时给你反馈正确值。
你能负担得起相对昂贵的预计算是因为,在用户与网站交互后有 100 ms 的空闲时间,这时你能够在用户不注意的时候进行操作。如果你在这段时间内完成了预计算,用户会感觉你的网站响应非常迅速!这只有在动画保持在 60 fps 的情况下才能体现。
我们能利用空闲期完成所有 getBoundingClientRect
工作(或 getComputedStyle
),从而确保动画能流畅地运行。另外,改变 transform
和 opacity
属性有利于渲染层合并。(想知道为什么会这样?看看我的 Pixels are Expensive 文章。)
transfrom 和 opacity 属性都非常适合动画。
transform
和 opacity
都非常适合动画。如果你们已经在 JS 和 CSS 动画里限制在这两个属性内,那是明智的选择。当你将布局属性(如 width、height、left 和 top)改为用这些对动画友好属性时,这种技术效果最好。
有时候你需要重新思考你的动画,以适应这种模式。而且,在很多时候我会分离和单独移动元素,从而能移动它们而不会扭曲,并尽可能多地使用 FLIP。你可能觉得这会使用过度,但我并不这样认为,原因有两点:
- 用户需要这样。我的同事兼朋友 Paul Kinlan 最近进行了一个调查,主题是人们最想从一个新闻 app 里得到什么。最多的答复竟然不是支持离线阅读、同步、通知或类似这样的东西(这至少让我觉得很惊奇!),而是一个流畅的导航。流畅就是不闪烁、不卡顿、不抖动。
- 用原生App开发。当然这是我个人见解,但我已多次听到原生应用开发者为了让应用程序得到恰到好处的过渡而花费很多时间,因为细节决定成败。正如我们通过 Service Worker 让网站更快地加载,所以我们都是朝着同样的目标而努力。用户会通过使用感受来评判我们网站的好坏。
一些注意事项
如果你使用 FLIP,则需牢记以下几点:
- 别超过 100 ms 的空闲期。重要的是要记住不要超过空闲期,否则你的 app 可能会出现无响应状态。如果你打破了这个界限,可通过开发者工具留意这个问题。
- 仔细安排你的动画。想象一下,如果你正在运行
transform
和opacity
动画中的一个,然后决定运行另一个而且需要进行大量的预计算。这会打断正在运行的动画,这是糟糕的。这里的关键在于,确保你的预计算工作是在空闲的时候运行,或考虑我谈论过的“响应时间”,此时这样两个动画就不会互相冲突。 - 内容会扭曲。当你应用 scale 和 transform 时,一些元素会被扭曲。正如我上述所说,我知道去重构我的标签以保证 FLIP 不会导致扭曲,但最终的结果可能是两种操作会互相影响。
FLIP 可以是……
我喜欢以 FLIP 作为关于动画的思考方式,因为这是对 JavaScript 和 CSS 的很好结合。用 JavaScript 计算,但让 CSS 为你处理动画。你不必使用 CSS 去完成动画,不过,你可以用 animations API 或 JavaScript 自身来完成,觉得哪种容易就用哪种。关键要减少每帧动画的复杂性(推荐使用 transform
和 opacity
),尽力让用户得到最好的体验。
我有更多关于 FLIP 东西和性能模型的相关内容想告诉你,但这将是下一篇博客的内容!