feat: support requestAnimationFrame (#8112)

* feat: support requestAnimationFrame

* add native helpers to measure time

* test(animation-frame): add tests

* chore: refactor animation-frame to its own module

* chore: fix tslint
This commit is contained in:
Eduardo Speroni
2019-11-29 06:17:26 -03:00
committed by Alexander Vakrilov
parent 0ffc790d82
commit 2aa6e9bf92
12 changed files with 209 additions and 0 deletions

View File

@ -0,0 +1,24 @@
/**
* Allows you to create, cancel and react to listeners to animation frames.
* @module "animation-frame"
*/ /** */
/**
* Callback called on frame rendered
* @argument time Time of the current frame in milliseconds
*/
export interface FrameRequestCallback {
(time: number): void;
}
/**
* Requests an animation frame and returns the timer ID
* @param cb Callback to be called on frame
*/
export function requestAnimationFrame(cb: FrameRequestCallback): number
/**
* Cancels a previously scheduled animation frame request
* @param id timer ID to cancel
*/
export function cancelAnimationFrame(id: number): void;

View File

@ -0,0 +1,62 @@
import { FPSCallback } from "../fps-meter/fps-native";
import { getTimeInFrameBase } from "./animation-native";
export interface FrameRequestCallback {
(time: number): void;
}
let animationId = 0;
let nextFrameAnimationCallbacks: { [key: string]: FrameRequestCallback } = {};
let shouldStop = true;
let inAnimationFrame = false;
let fpsCallback: FPSCallback;
let lastFrameTime = 0;
function getNewId() {
return animationId++;
}
function ensureNative() {
if (fpsCallback) {
return;
}
fpsCallback = new FPSCallback(doFrame);
}
function doFrame(currentTimeMillis: number) {
lastFrameTime = currentTimeMillis;
shouldStop = true;
const thisFrameCbs = nextFrameAnimationCallbacks;
nextFrameAnimationCallbacks = {};
inAnimationFrame = true;
for (const animationId in thisFrameCbs) {
if (thisFrameCbs[animationId]) {
thisFrameCbs[animationId](lastFrameTime);
}
}
inAnimationFrame = false;
if (shouldStop) {
fpsCallback.stop(); // TODO: check performance without stopping to allow consistent frame times
}
}
export function requestAnimationFrame(cb: FrameRequestCallback): number {
if (!inAnimationFrame) {
inAnimationFrame = true;
zonedCallback(cb)(getTimeInFrameBase()); // TODO: store and use lastFrameTime
inAnimationFrame = false;
return getNewId();
}
ensureNative();
const animId = getNewId();
nextFrameAnimationCallbacks[animId] = zonedCallback(cb) as FrameRequestCallback;
shouldStop = false;
fpsCallback.start();
return animId;
}
export function cancelAnimationFrame(id: number) {
delete nextFrameAnimationCallbacks[id];
}

View File

@ -0,0 +1,4 @@
export function getTimeInFrameBase(): number {
return java.lang.System.nanoTime() / 1000000;
}

View File

@ -0,0 +1,4 @@
/**
* Gets the time in millisseconds in the same base as frames
*/
export function getTimeInFrameBase(): number;

View File

@ -0,0 +1,3 @@
import { time } from "../profiling";
export const getTimeInFrameBase = time;

View File

@ -0,0 +1,6 @@
{
"name": "animation-frame",
"main": "animation-frame",
"types": "animation-frame.d.ts",
"nativescript": {}
}

View File

@ -1,6 +1,7 @@
import "./core";
import "./polyfills/timers";
import "./polyfills/animation";
import "./polyfills/dialogs";
import "./polyfills/xhr";
import "./polyfills/fetch";

View File

@ -0,0 +1,5 @@
/**
* Installs animations polyfill.
* @module "globals/polyfills/animations"
*/ /** */
import "../../core";

View File

@ -0,0 +1,6 @@
import "../../core";
import { installPolyfills } from "../polyfill-helpers";
global.registerModule("animation", () => require("../../../animation-frame"));
installPolyfills("animation", ["requestAnimationFrame", "cancelAnimationFrame"]);

View File

@ -0,0 +1,6 @@
{
"name": "animation",
"main": "animation",
"types": "animation.d.ts",
"nativescript": {}
}

View File

@ -0,0 +1,85 @@
import * as TKUnit from "../tk-unit";
import * as animationFrame from "@nativescript/core/animation-frame";
import * as fpsNative from "@nativescript/core/fps-meter/fps-native";
export function test_requestAnimationFrame_isDefined() {
TKUnit.assertNotEqual(animationFrame.requestAnimationFrame, undefined, "Method animationFrame.requestAnimationFrame() should be defined!");
}
export function test_cancelAnimationFrame_isDefined() {
TKUnit.assertNotEqual(animationFrame.cancelAnimationFrame, undefined, "Method animationFrame.cancelAnimationFrame() should be defined!");
}
export function test_requestAnimationFrame() {
let completed: boolean;
const id = animationFrame.requestAnimationFrame(() => {
completed = true;
});
TKUnit.waitUntilReady(() => completed, 0.5, false);
animationFrame.cancelAnimationFrame(id);
TKUnit.assert(completed, "Callback should be called!");
}
export function test_requestAnimationFrame_callbackCalledInCurrentFrame() {
let completed: boolean;
let currentFrameTime = 0;
const frameCb = new fpsNative.FPSCallback((time) => {
currentFrameTime = time;
});
frameCb.start();
TKUnit.waitUntilReady(() => currentFrameTime > 0, 0.5);
let calledTime = 0;
animationFrame.requestAnimationFrame((frameTime) => {
calledTime = frameTime;
completed = calledTime >= frameTime;
});
TKUnit.waitUntilReady(() => completed, 0.5, false);
frameCb.stop();
TKUnit.assert(completed, "Callback should be called in current frame!");
}
export function test_requestAnimationFrame_nextCallbackCalledInNextFrame() {
let completed: boolean;
let currentFrameTime = 0;
const frameCb = new fpsNative.FPSCallback((time) => {
currentFrameTime = time;
});
frameCb.start();
TKUnit.waitUntilReady(() => currentFrameTime > 0, 0.5);
animationFrame.requestAnimationFrame((firstFrameTime) => {
animationFrame.requestAnimationFrame((frameTime) => {
frameCb.stop();
completed = frameTime > firstFrameTime && frameTime === currentFrameTime;
});
});
TKUnit.waitUntilReady(() => completed, 0.5, false);
frameCb.stop();
TKUnit.assert(completed, "Callback should be called in next frame!");
}
export function test_requestAnimationFrame_shouldBeCancelled() {
let completed: boolean;
let currentFrameTime = 0;
const frameCb = new fpsNative.FPSCallback((time) => {
currentFrameTime = time;
});
frameCb.start();
TKUnit.waitUntilReady(() => currentFrameTime > 0, 0.5);
animationFrame.requestAnimationFrame((firstFrameTime) => {
const cbId = animationFrame.requestAnimationFrame((frameTime) => {
completed = true;
});
animationFrame.cancelAnimationFrame(cbId);
});
TKUnit.wait(1);
frameCb.stop();
TKUnit.assert(!completed, "Callback should not be called");
}

View File

@ -76,6 +76,9 @@ allTests["OBSERVABLE"] = observableTests;
import * as timerTests from "./timer/timer-tests";
allTests["TIMER"] = timerTests;
import * as animationFrameTests from "./animation-frame/animation-frame";
allTests["ANIMATION-FRAME"] = animationFrameTests;
import * as colorTests from "./color/color-tests";
allTests["COLOR"] = colorTests;