小动画大学问

对于移动端的Web单页应用来讲,为了达到媲美原生应用的效果,页面过渡动画是必不可少的。经常使用的页面过渡动画包括:css

  1. 位移——当前页向左侧或右侧水平移出可视区,下一页由反方向移入可视区。
  2. 不透明度变化——当前页淡出,下一页淡入。
  3. 1和2同时进行。

(注意:如下讨论和实验均在 Chrome 68 浏览器环境下进行)html

目前大多数设备的屏幕刷新率为60次/秒,算下来每一个帧的预算时间约为16.66毫秒(1/60秒)。考虑到浏览器还有其余工做要执行,实际上预算时间只有10毫秒。跟此预算时间的差值越大,用户就会以为动画过程越卡。那么,在这10毫秒内要完成什么事情呢?当使用JavaScript实现视觉交互效果时,通常要通过如下流程:web

JavaScript视觉交互执行流程

  1. JavaScript的执行。例如修改元素的样式,或者给元素添加/删除样式类。
  2. 样式计算。根据样式规则计算出元素的最终样式。
  3. 布局(layout)。根据上一步的结果,计算元素占据的空间大小及其在屏幕的位置。注意,一个元素布局上的变化有可能会引起其余元素的联动变化。
  4. 绘制(paint)。填充像素的过程,包括元素的每一个可视部分。通常来讲,绘制是在多个层上进行的。
  5. 合成(composite)。把各层按正确顺序合并成一个层,显示到屏幕上。

值得注意的是,并不是每一帧都会通过上述每个步骤的处理。若是元素的几何属性(尺寸、位置)没有变化,就不须要进行布局;若是连元素的外观都没有改变,就不须要绘制。因此,实现流畅动画的关键就在于如何减小布局和绘制浏览器

位移

对于位移动画来讲,最直接的实现方式,就是把元素设成绝对定位,而后去改变它的left样式值。例如:性能优化

<!DOCTYPE html>
<html>
<head>
<style>
.page {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    min-height: 100%;
    background: #ddd;
    transition-duration: 2s;
    transition-property: left;
}
.leave {
    left: -100%;
}
</style>
</head>

<body>
<div id="page" class="page"></div>
<script>
var page = document.getElementById('page');
setTimeout(function() {
    page.classList.add('leave');
}, 2000);
</script>
</body>
</html>

使用Chrome开发者工具中的Performance面板录制动画过程的性能日志,以下图所示:app

left动画过程性能日志

可见,元素在移动的过程当中不断触发了布局和绘制。因此,这种实现方式的性能是极低的。网上诸多文献会推荐以transform的变化代替left的变化,而实际状况又是怎么样呢?把样式代码稍做修改:框架

.page {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    min-height: 100%;
    background: #ddd;
    transition-duration: 2s;
    transition-property: transform;
}
.leave {
    transform: translateX(-100%);
}

录制性能日志以下图所示:工具

transform性能日志

可见,仅仅是在动画开始和结束两个时间点触发了绘制,而布局则彻底没有触发。这样一来,性能就有了很大的提高。可是,这里还有两个疑问:布局

  • 为何transform动画过程没有触发布局和绘制?
  • 为何动画开始前触发了两次绘制,动画结束以后触发了一次绘制?

要回答这两个问题,就得了解合成层。性能

合成层

当知足某些条件的时候,元素在渲染时会被分配到一个独立的层中进行渲染,只要该层的内容不发生改变,就不会触发绘制,浏览器会直接经过合成造成一个新的帧。常见的提高为合成层的条件包括:

  • 对opacity或transform应用了animation或transition;
  • 有 3D transform ;
  • will-change设置为opacity或transform。

很明显,上一节的transform位移动画知足了第一个条件。因此整个动画的渲染过程是这样的:

  • 动画开始时,因为div.page被提高为独立的合成层,因此它要从新绘制;而document所在层至关于少了一块内容,也得从新绘制;
  • 动画过程当中,div.page没有其余变化,因此不触发布局和绘制;
  • 动画结束后,div.page再也不是独立的合成层,回到了document所在层,因此document又从新绘制了一遍。

若是让div.page一直在独立的合成层中渲染,则能够省掉上述过程当中绘制的环节。在样式代码添加「will-change: transform」:

.page {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    min-height: 100%;
    background: #ddd;
    transition-duration: 2s;
    transition-property: transform;
    will-change: transform;
}

录制性能日志以下:

合成层transform性能日志

可见,已经不存在绘制的步骤了。

顺带一提,Chrome开发者工具中有一个Layers面板,能够方便地查看页面上合成层以及成为合成层的缘由。

合成层transform性能日志

(注意:因为低版本浏览器不支持will-change,因此实际应用中,若是想把元素提高到独立的合成层中渲染,能够用「transform: translateZ(0)」)

不透明度

众所周知,不透明度就是经过opacity样式来控制的。那么opacity的变化是否会触发布局和绘制呢?把样式代码修改以下:

.page {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    min-height: 100%;
    background: #ddd;
    transition-duration: 2s;
    transition-property: opacity;
}
.leave {
    opacity: 0;
}

录制性能日志以下图所示:

opacity性能日志

在常规认知中,opacity的变化并不会致使元素位置和尺寸的变化,理应不会触发布局。但上述过程当中确实触发了一次布局,表现较为诡异。接下来给div.page添加「will-change: opacity」使其一直在独立的合成层中渲染。录制性能日志以下:

opacity合成层性能日志

可见,仍是会触发一次绘制。而针对这「一次的布局」和「一次的绘制」,我进行了进一步的实验,得出的结论是:opacity从1(包括未设置的状况,下同)变动到小于1,以及从小于1变动到1,都会触发布局和绘制;即便在独立的合成层中渲染,也只能省掉布局,没法省掉绘制。

因为在opacity动画过程当中从1到小于1的变动只会有一次,因此上述的布局和绘制都只触发一次。

位移和不透明度

同时使用两种动画,修改样式代码以下:

.page {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    min-height: 100%;
    background: #ddd;
    transition-duration: 2s;
    transition-property: transform, opacity;
}
.leave {
    transform: translateX(-100%);
    opacity: 0;
}

按照前文的描述,动画过程会触发:

  • 一次布局,在动画开始时触发,由opacity引发;
  • 两次绘制,在动画开始时触发,因opacity以及提高为独立合成层引发;
  • 由独立合成层回到document所在层时引发。

假若加上「will-change: transform, opacity」,使div.page一直在独立的合成层中渲染,则只触发一次绘制,由opacity引发。

然而,建立一个新的合成层并非免费的,它会致使额外的内存开销。在单页应用中,应用页面过渡动画的元素是页面的最外层容器,包含了该页面全部内容结构。若是让其长期在独立的合成层中渲染,那内存的消耗是很是大的。

因此,能够仅在动画过程当中让其在独立的合成层中渲染,而在其余状况下则维持常规状态。

transform和fixed的冲突

若是用transform实现页面过渡动画,想必你们都遇到过一个问题:页面上固定定位的元素,其位置变得不太正常了。

下面经过一段代码模拟页面进入的过程,来演示这个问题:

<!DOCTYPE html>
<html>
<head>
<style>
.page {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 150%;
    background: #ddd;
    transition-duration: 3s;
    transition-timing-function: cubic-bezier(.55, 0, .1, 1);
    transition-property: transform, opacity;
}
.before-enter {
    transform: translateX(100%);
    opacity: 0;
}
.fixed {
    position: fixed;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 160px;
    background: #ffc100;
}
</style>
</head>

<body>
<div id="page" class="page before-enter">
    <div class="fixed"></div>
</div>
<script>
var page = document.getElementById('page');
setTimeout(() => {
    page.classList.remove('before-enter');
}, 2000);
</script>
</body>
</html>

运行效果以下:

transform和fixed的冲突

能够看到,固定定位的黄色元素是在动画结束后才忽然出现的。那在这以前它跑到哪去了呢?

若是给一个固定定位元素的任意一个祖先元素设置样式「transform」或者「will-change: transform」,那么该元素就会相对于最近的设置了上述样式的祖先元素定位。

由于div.page的高度设成了150%,因此,在动画过程当中,黄色元素其实是跑到了页面的最底下(超出了浏览器可视范围)去了。而在某些比较旧(如 iOS 9 的Safari)的移动端浏览器中,问题更为严重,固定定位的元素可能会消失掉不再出现。

网上能查到的解决方案有两种:

  • 经过绝对定位模拟固定定位。虽然是可行的,可是在移动端浏览器内,交互上会有一些细节问题,并且元素内部的滚动很容易与页面滚动冲突。
  • 把固定定位的元素放到应用transform动画的元素外。但这对使用「Vue.js」这类框架开发的单页应用来讲可行性较低,由于在这类框架中,一个页面就是一个组件,单独把页面中的某个元素抽离出来是比较麻烦的。

因此,这里介绍第三种方案——在页面过渡动画结束以后(此时transform样式已被移除,再也不影响fixed),再让固定定位的元素插入到页面容器。而且,为了让它的出现显得不那么忽然,增长缓动动画。代码主要修改点以下:

@keyframes kf-move-in {
    0% { transform: translateY(100%); }
    100% { transform: translateY(0); }
}
.move-in {
    animation-name: kf-move-in;
    animation-duration: 0.45s;
}
<div id="page" class="page before-enter"></div>
<script>
var page = document.getElementById('page');
setTimeout(function() {
    // 监听过渡结束
    page.addEventListener('transitionend', function() {
        // 建立、插入固定定位元素
        var div = document.createElement('div');
        div.className = 'fixed move-in';
        page.appendChild(div);
    });

    page.classList.remove('before-enter');
}, 2000);
</script>

运行效果以下:

解决transform和fixed的冲突

这样一来,整个交互就较为友好了。这同时也说明:技术上的问题,不必定只能经过技术去解决,也能够从交互上去寻求解决方案。

参考文献

本文同时发布于做者我的博客: https://mrluo.life/article/de...

相关文章
相关标签/搜索