From 2aa6e9bf922286f9ffd747f901dbc922cd9446b9 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 29 Nov 2019 06:17:26 -0300 Subject: [PATCH] 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 --- .../animation-frame/animation-frame.d.ts | 24 ++++++ .../animation-frame/animation-frame.ts | 62 ++++++++++++++ .../animation-native.android.ts | 4 + .../animation-frame/animation-native.d.ts | 4 + .../animation-frame/animation-native.ios.ts | 3 + .../animation-frame/package.json | 6 ++ nativescript-core/globals/globals.ts | 1 + .../polyfills/animation/animation.d.ts | 5 ++ .../globals/polyfills/animation/animation.ts | 6 ++ .../globals/polyfills/animation/package.json | 6 ++ tests/app/animation-frame/animation-frame.ts | 85 +++++++++++++++++++ tests/app/test-runner.ts | 3 + 12 files changed, 209 insertions(+) create mode 100644 nativescript-core/animation-frame/animation-frame.d.ts create mode 100644 nativescript-core/animation-frame/animation-frame.ts create mode 100644 nativescript-core/animation-frame/animation-native.android.ts create mode 100644 nativescript-core/animation-frame/animation-native.d.ts create mode 100644 nativescript-core/animation-frame/animation-native.ios.ts create mode 100644 nativescript-core/animation-frame/package.json create mode 100644 nativescript-core/globals/polyfills/animation/animation.d.ts create mode 100644 nativescript-core/globals/polyfills/animation/animation.ts create mode 100644 nativescript-core/globals/polyfills/animation/package.json create mode 100644 tests/app/animation-frame/animation-frame.ts diff --git a/nativescript-core/animation-frame/animation-frame.d.ts b/nativescript-core/animation-frame/animation-frame.d.ts new file mode 100644 index 000000000..c899d0901 --- /dev/null +++ b/nativescript-core/animation-frame/animation-frame.d.ts @@ -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; diff --git a/nativescript-core/animation-frame/animation-frame.ts b/nativescript-core/animation-frame/animation-frame.ts new file mode 100644 index 000000000..6a23bc771 --- /dev/null +++ b/nativescript-core/animation-frame/animation-frame.ts @@ -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]; +} diff --git a/nativescript-core/animation-frame/animation-native.android.ts b/nativescript-core/animation-frame/animation-native.android.ts new file mode 100644 index 000000000..1dee2bbd0 --- /dev/null +++ b/nativescript-core/animation-frame/animation-native.android.ts @@ -0,0 +1,4 @@ + +export function getTimeInFrameBase(): number { + return java.lang.System.nanoTime() / 1000000; +} diff --git a/nativescript-core/animation-frame/animation-native.d.ts b/nativescript-core/animation-frame/animation-native.d.ts new file mode 100644 index 000000000..884449e1d --- /dev/null +++ b/nativescript-core/animation-frame/animation-native.d.ts @@ -0,0 +1,4 @@ +/** + * Gets the time in millisseconds in the same base as frames + */ +export function getTimeInFrameBase(): number; \ No newline at end of file diff --git a/nativescript-core/animation-frame/animation-native.ios.ts b/nativescript-core/animation-frame/animation-native.ios.ts new file mode 100644 index 000000000..45cac4da7 --- /dev/null +++ b/nativescript-core/animation-frame/animation-native.ios.ts @@ -0,0 +1,3 @@ +import { time } from "../profiling"; + +export const getTimeInFrameBase = time; diff --git a/nativescript-core/animation-frame/package.json b/nativescript-core/animation-frame/package.json new file mode 100644 index 000000000..a50f09ab4 --- /dev/null +++ b/nativescript-core/animation-frame/package.json @@ -0,0 +1,6 @@ +{ + "name": "animation-frame", + "main": "animation-frame", + "types": "animation-frame.d.ts", + "nativescript": {} +} diff --git a/nativescript-core/globals/globals.ts b/nativescript-core/globals/globals.ts index e88a68496..005216cfb 100644 --- a/nativescript-core/globals/globals.ts +++ b/nativescript-core/globals/globals.ts @@ -1,6 +1,7 @@ import "./core"; import "./polyfills/timers"; +import "./polyfills/animation"; import "./polyfills/dialogs"; import "./polyfills/xhr"; import "./polyfills/fetch"; diff --git a/nativescript-core/globals/polyfills/animation/animation.d.ts b/nativescript-core/globals/polyfills/animation/animation.d.ts new file mode 100644 index 000000000..e149a301d --- /dev/null +++ b/nativescript-core/globals/polyfills/animation/animation.d.ts @@ -0,0 +1,5 @@ +/** + * Installs animations polyfill. + * @module "globals/polyfills/animations" + */ /** */ +import "../../core"; diff --git a/nativescript-core/globals/polyfills/animation/animation.ts b/nativescript-core/globals/polyfills/animation/animation.ts new file mode 100644 index 000000000..6c38fa34b --- /dev/null +++ b/nativescript-core/globals/polyfills/animation/animation.ts @@ -0,0 +1,6 @@ +import "../../core"; +import { installPolyfills } from "../polyfill-helpers"; + +global.registerModule("animation", () => require("../../../animation-frame")); + +installPolyfills("animation", ["requestAnimationFrame", "cancelAnimationFrame"]); diff --git a/nativescript-core/globals/polyfills/animation/package.json b/nativescript-core/globals/polyfills/animation/package.json new file mode 100644 index 000000000..a647c1dd4 --- /dev/null +++ b/nativescript-core/globals/polyfills/animation/package.json @@ -0,0 +1,6 @@ +{ + "name": "animation", + "main": "animation", + "types": "animation.d.ts", + "nativescript": {} +} \ No newline at end of file diff --git a/tests/app/animation-frame/animation-frame.ts b/tests/app/animation-frame/animation-frame.ts new file mode 100644 index 000000000..a0d6b87c9 --- /dev/null +++ b/tests/app/animation-frame/animation-frame.ts @@ -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"); +} diff --git a/tests/app/test-runner.ts b/tests/app/test-runner.ts index 060216722..da5cee296 100644 --- a/tests/app/test-runner.ts +++ b/tests/app/test-runner.ts @@ -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;