js_frame_anim

JS 帧动画

背景

我们经常需要在 JS 中通过定时器实现多帧的绘制实现动画效果。例如我们要使用画布实现一个动画效果,可能会这样写:

var that = this
var progress = 0
this.timer = setInterval(function () {
    if (progress <= 1) {
        renderFrame(progress) // 这是绘制一帧的代码
        progress += 0.01
    } else {
        clearInterval(that.timer)
    }
}, 50)

这段代码使用一个超时间隔为 50ms 的定时器来驱动动画的播放。这种做法在模拟器中一般没有什么问题,但是在硬件中却可能没法运行。主要原因是定时器使用固定的超时间隔,这会导致在绘图的帧率达不到定时器超时频率时,定时器的超时事件会堆积在 GUI 的事件队列中,而这些堆积的超时事件会进一步降低帧率。

即使通过调整定时器超时间隔也无法很好地解决上述问题。例如,假设实际硬件执行 renderFrame() 函数的最大帧率为 10,我们把定时器超时间隔设置为临界的 100ms。动画开始后的一段时间里可能会很正常,但是一旦发生稍微耗时的操作都将导致定时器回调不及时,这会立即导致定时器超时事件堆积并使帧率会持续下降。

由于定时器无法避免这类操作最终会不可逆转的掉帧甚至卡死。建议改用 requestAnimationFrame() 方法实现动画机制。

requestAnimationFrame

原型

requestAnimationFrame(callback :: function)

requestAnimationFrame 是专门用于实现 JS 帧动画的函数,它向柿饼 UI 注册一个函数,而柿饼会在下一次重绘之前调用该函数以更新动画效果。该函数需要传入一个回调函数作为参数,该回调函数会在下一次 UI 重绘前调用。

如果要绘制多帧的动画,需要在回调函数中再次调用 requestAnimationFrame

你需要在准备更新动画的时候调用此函数。在不掉帧的情况下,回调函数的执行频率和屏幕的刷新率相同(通常是 60Hz)。但是不能假设回调函数以一个固定的频率被执行,例如不同的设备可能有不同的刷新帧率,该函数的执行频率也可能受到性能影响而变化。

回调函数会被传入一个时间戳参数,它指示回调函数被执行时的时刻(单位是毫秒)。

该函数没有返回值。

范例

这里演示用 requestAnimationFrame 改写之前使用定时器驱动的动画流程:

var start = null
function renderCallback(timestamp) {
    if (!start) start = timestamp // record start time
    var progress = (timestamp - start) / 500 // animation duration is 500ms
    if (progress > 1) progress = 1 // we assume that progress must be between [0, 1]
    renderFrame(progress) // render next frame
    if (progress < 1) { // request next frame
        requestAnimationFrame(renderCallback)
    }
}
requestAnimationFrame(renderCallback) // start first frame

这段代码中,我们假设动画的执行时间为 0.5s,且 renderFrame 函数接受 [0, 1] 范围的进度值作为输入。renderCallback 通过 timerstamp 参数获得的时间来计算动画的播放进度,这样可以保证在任何环境下动画速度相同。

关于时间戳

还可以使用 pm.clock() 方法获取时间戳,函数返回的时间戳也是以毫秒为单位。

基于 page 的 requestAnimationFrame

Page 对象也提供了一个 requestAnimationFrame 方法,与全局的版本相比,前者具还具有以下特性:

  • Page 隐藏时该方法不会调用回调函数(切换到新的 page 后旧的 page 通常会隐藏),从而减少运算量
  • Page 销毁时会自动清理而不会导致崩溃

注意:由于 page.requestAnimationFrame 是绑定 page 的,它的稳定性和优化余地要大于全局版本,因此建议使用 page 的 requestAnimationFrame 方法来驱动动画渲染。

pm.clock 方法

此方法返回一个时间戳,单位为毫秒。此时间一般是从系统启动开始计算的时间,可以通过两次调用 pm.clock() 方法的返回值来计算时间差:

var tick = pm.clock()
// ... 某些操作
console.log('time: ' + (pm.clock() - tick)) // 打印上段操作的耗时

在 Page 中使用的例子

下面介绍一个使用 requestAnimationFrame 实现控件移动的动画效果:

var page = {
    onLoad: function (event) {},
    onButton: function(event) {
        var start = null, that = this
        function renderCallback(timestamp) {
            if (!start) start = timestamp // 记录开始时间
            var progress = (timestamp - start) / 500 // 动画持续 500ms
            if (progress > 1) progress = 1 // 进度保持在 [0, 1] 之间
            that.setData({ button1: { position: { x: progress * 100 } }}) // 更新一帧状态
            if (progress < 1) { // 请求绘制下一帧
                requestAnimationFrame(renderCallback)
            }
        }
        requestAnimationFrame(renderCallback) // 请求第一帧
    }
};
Page(page);

这个例子的执行效果如下:

sample