FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: expose requestAnimationFrame under the standard global names · NativeScript/android@d1fc925 · GitHub

Commit d1fc925

Browse files
committed
feat: expose requestAnimationFrame under the standard global names
Register requestAnimationFrame/cancelAnimationFrame on every global (workers included), built on the existing frame-callback machinery with spec semantics: one one-shot entry per request, a returned handle, cancellation by handle, and a single performance-timeline timestamp argument. The __postFrameCallback/__removeFrameCallback pair stays as the backwards-compatible surface.
1 parent 4da75c2 commit d1fc925

3 files changed

Lines changed: 192 additions & 10 deletions

File tree

‎test-app/app/src/main/assets/app/tests/testPostFrameCallback.js‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,100 @@ describe("test PostFrameCallback", function () {
141141
});
142142
});
143143

144+
describe("requestAnimationFrame", function () {
145+
const defaultWaitTime = 300;
146+
147+
it("is exposed under the standard global names", () => {
148+
expect(typeof global.requestAnimationFrame).toBe("function");
149+
expect(typeof global.cancelAnimationFrame).toBe("function");
150+
});
151+
152+
it("throws on non-function callbacks and ignores bogus cancel handles", () => {
153+
expect(() => global.requestAnimationFrame()).toThrowError(TypeError);
154+
expect(() => global.requestAnimationFrame(null)).toThrowError(TypeError);
155+
expect(() => global.requestAnimationFrame("")).toThrowError(TypeError);
156+
expect(() => global.cancelAnimationFrame()).not.toThrow();
157+
expect(() => global.cancelAnimationFrame(null)).not.toThrow();
158+
expect(() => global.cancelAnimationFrame(-1)).not.toThrow();
159+
expect(() => global.cancelAnimationFrame(Number.MAX_SAFE_INTEGER)).not.toThrow();
160+
});
161+
162+
it("returns a handle and passes a single performance-timeline timestamp", (done) => {
163+
const handle = global.requestAnimationFrame(function (timestamp) {
164+
expect(arguments.length).toBe(1);
165+
expect(typeof timestamp).toBe("number");
166+
const now = performance.now();
167+
expect(timestamp).toBeGreaterThan(0);
168+
expect(timestamp).not.toBeGreaterThan(now);
169+
expect(now - timestamp).toBeLessThan(250);
170+
done();
171+
});
172+
expect(typeof handle).toBe("number");
173+
expect(handle).toBeGreaterThan(0);
174+
});
175+
176+
it("runs the same function once per request", (done) => {
177+
let callCount = 0;
178+
const callback = () => {
179+
callCount++;
180+
};
181+
const first = global.requestAnimationFrame(callback);
182+
const second = global.requestAnimationFrame(callback);
183+
expect(second).not.toBe(first);
184+
setTimeout(() => {
185+
expect(callCount).toBe(2);
186+
done();
187+
}, defaultWaitTime);
188+
});
189+
190+
it("cancels only the cancelled request", (done) => {
191+
let cancelledRan = false;
192+
let keptRan = false;
193+
const cancelled = global.requestAnimationFrame(() => {
194+
cancelledRan = true;
195+
});
196+
global.requestAnimationFrame(() => {
197+
keptRan = true;
198+
});
199+
global.cancelAnimationFrame(cancelled);
200+
setTimeout(() => {
201+
expect(cancelledRan).toBe(false);
202+
expect(keptRan).toBe(true);
203+
done();
204+
}, defaultWaitTime);
205+
});
206+
207+
it("chains frames when the callback re-requests itself", (done) => {
208+
const timestamps = [];
209+
const callback = (timestamp) => {
210+
timestamps.push(timestamp);
211+
if (timestamps.length === 1) {
212+
global.requestAnimationFrame(callback);
213+
}
214+
};
215+
global.requestAnimationFrame(callback);
216+
setTimeout(() => {
217+
expect(timestamps.length).toBe(2);
218+
expect(timestamps[1]).not.toBeLessThan(timestamps[0]);
219+
done();
220+
}, defaultWaitTime);
221+
});
222+
223+
it("does not disturb __postFrameCallback dedupe for the same function", (done) => {
224+
let callCount = 0;
225+
const callback = () => {
226+
callCount++;
227+
};
228+
global.__postFrameCallback(callback);
229+
global.requestAnimationFrame(callback);
230+
global.__postFrameCallback(callback);
231+
setTimeout(() => {
232+
expect(callCount).toBe(2);
233+
done();
234+
}, defaultWaitTime);
235+
});
236+
});
237+
144238
// The two implementations behind __postFrameCallback (NDK AChoreographer,
145239
// android.view.Choreographer for API < 24) must be indistinguishable from JS.
146240
// A modern device always selects the NDK one, so the Java bridge is only

‎test-app/runtime/src/main/cpp/FrameCallbacks.cpp‎

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,12 @@ void ResolveFrameCallbacksClass(JEnv& env) {
151151
using EntryId = uintptr_t;
152152

153153
struct FrameCallbackEntry {
154-
FrameCallbackEntry(Isolate* isolate, Local<Function> callback, EntryId id)
155-
: isolate_(isolate), callback_(isolate, callback), id_(id) {
154+
FrameCallbackEntry(Isolate* isolate, Local<Function> callback, EntryId id,
155+
bool isAnimationFrame = false)
156+
: isolate_(isolate),
157+
callback_(isolate, callback),
158+
id_(id),
159+
isAnimationFrame_(isAnimationFrame) {
156160
}
157161

158162
~FrameCallbackEntry() {
@@ -200,6 +204,13 @@ struct FrameCallbackEntry {
200204
Isolate* isolate_;
201205
Global<Function> callback_;
202206
EntryId id_;
207+
/*
208+
* requestAnimationFrame entries are anonymous one-shots addressed only by
209+
* their returned handle: they never dedupe by function, take the single
210+
* spec-mandated timestamp argument, and cancelAnimationFrame may only
211+
* touch entries carrying this flag.
212+
*/
213+
bool isAnimationFrame_;
203214
jobject javaCallback_ = nullptr;
204215

205216
private:
@@ -273,15 +284,23 @@ void Dispatch(EntryId id, int64_t frameTimeNanos) {
273284

274285
entry->MarkUnscheduled();
275286

287+
Local<Value> timelineMillis =
288+
Number::New(isolate, Performance::MonotonicNanosToTimelineMillis(
289+
isolate, frameTimeNanos));
276290
Local<Value> args[2] = {
277291
Number::New(isolate, (double) frameTimeNanos),
278-
Number::New(isolate, Performance::MonotonicNanosToTimelineMillis(
279-
isolate, frameTimeNanos)),
292+
timelineMillis,
280293
};
281294

282295
TryCatch tc(isolate);
283296

284-
cb->Call(context, context->Global(), 2, args); // ignore JS return value
297+
if (entry->isAnimationFrame_) {
298+
// spec signature: a single DOMHighResTimeStamp on this isolate's
299+
// performance timeline
300+
cb->Call(context, context->Global(), 1, &timelineMillis);
301+
} else {
302+
cb->Call(context, context->Global(), 2, args); // ignore JS return value
303+
}
285304

286305
// Re-resolve: the callback may have rescheduled or removed itself.
287306
entry = FindEntryById(id);
@@ -436,6 +455,58 @@ void FrameCallbacks::RemoveFrameCallback(const FunctionCallbackInfo<Value>& args
436455
}
437456
}
438457

458+
void FrameCallbacks::RequestAnimationFrame(const FunctionCallbackInfo<Value>& args) {
459+
Isolate* isolate = args.GetIsolate();
460+
Locker locker(isolate);
461+
Isolate::Scope isolateScope(isolate);
462+
HandleScope handleScope(isolate);
463+
Local<Context> context = isolate->GetCurrentContext();
464+
Context::Scope contextScope(context);
465+
466+
if (args.Length() < 1 || !args[0]->IsFunction()) {
467+
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8Literal(
468+
isolate, "Animation frame callback argument is not a function")));
469+
return;
470+
}
471+
472+
// Every call registers its own entry: the spec runs a function as many
473+
// times as it was requested, so no dedupe by function here.
474+
EntryId id = ++entryCount_;
475+
FrameCallbackEntry* entry;
476+
{
477+
std::lock_guard<std::mutex> lock(entriesMutex_);
478+
auto inserted = entries_.emplace(
479+
id, std::make_unique<FrameCallbackEntry>(
480+
isolate, args[0].As<Function>(), id,
481+
/* isAnimationFrame */ true));
482+
NS_DCHECK(inserted.second && "Frame callback ID should not be duplicated");
483+
entry = inserted.first->second.get();
484+
}
485+
486+
entry->MarkScheduled();
487+
Post(entry, 0);
488+
args.GetReturnValue().Set(Number::New(isolate, (double) id));
489+
}
490+
491+
void FrameCallbacks::CancelAnimationFrame(const FunctionCallbackInfo<Value>& args) {
492+
Isolate* isolate = args.GetIsolate();
493+
Locker locker(isolate);
494+
Isolate::Scope isolateScope(isolate);
495+
HandleScope handleScope(isolate);
496+
Local<Context> context = isolate->GetCurrentContext();
497+
Context::Scope contextScope(context);
498+
499+
// per spec an unknown or malformed handle is a silent no-op
500+
if (args.Length() < 1 || !args[0]->IsNumber()) {
501+
return;
502+
}
503+
EntryId id = (EntryId) args[0]->IntegerValue(context).FromMaybe(0);
504+
FrameCallbackEntry* entry = FindEntryById(id);
505+
if (entry != nullptr && entry->isAnimationFrame_) {
506+
entry->MarkRemoved();
507+
}
508+
}
509+
439510
void FrameCallbacks::RemoveIsolateEntries(Isolate* isolate) {
440511
// Detached first, destroyed after the mutex is released: the destructors
441512
// call into Java.
@@ -454,6 +525,15 @@ void FrameCallbacks::RemoveIsolateEntries(Isolate* isolate) {
454525
}
455526

456527
void FrameCallbacks::Init(Isolate* isolate, Local<ObjectTemplate> globalTemplate) {
528+
globalTemplate->Set(
529+
ArgConverter::ConvertToV8String(isolate, "requestAnimationFrame"),
530+
FunctionTemplate::New(isolate, RequestAnimationFrame));
531+
globalTemplate->Set(
532+
ArgConverter::ConvertToV8String(isolate, "cancelAnimationFrame"),
533+
FunctionTemplate::New(isolate, CancelAnimationFrame));
534+
// kept for backwards compatibility with callers that predate
535+
// requestAnimationFrame; unlike it, these dedupe by function, take an
536+
// optional delay, and hand the callback the raw frame time as well
457537
globalTemplate->Set(
458538
ArgConverter::ConvertToV8String(isolate, "__postFrameCallback"),
459539
FunctionTemplate::New(isolate, PostFrameCallback));

‎test-app/runtime/src/main/cpp/FrameCallbacks.h‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010
namespace tns {
1111

1212
/*
13-
* __postFrameCallback(fn[, delayMillis]) / __removeFrameCallback(fn): schedule
14-
* a JS function for the next display frame.
13+
* requestAnimationFrame(fn) / cancelAnimationFrame(handle): the standard
14+
* animation-frame surface. Each request registers its own one-shot entry and
15+
* returns a handle; fn receives a single DOMHighResTimeStamp on the isolate's
16+
* performance timeline.
17+
*
18+
* __postFrameCallback(fn[, delayMillis]) / __removeFrameCallback(fn): the
19+
* compatibility surface predating requestAnimationFrame. Schedules a JS
20+
* function for the next display frame, deduping by function: rescheduling a
21+
* callback that is already pending is a no-op.
1522
*
1623
* fn receives two arguments:
1724
* fn(frameTimeNanos, performanceMillis)
@@ -25,9 +32,8 @@ namespace tns {
2532
* - android.view.Choreographer (com.tns.FrameCallbacks), for API 21-23,
2633
* where the NDK API does not exist.
2734
* Scheduling is per calling thread, so a worker schedules against its own
28-
* looper. Rescheduling a callback that is already pending is a no-op, and
29-
* removal only marks the entry: a posted frame callback cannot be recalled, so
30-
* the dispatch drops it instead.
35+
* looper. Cancellation on either surface only marks the entry: a posted frame
36+
* callback cannot be recalled, so the dispatch drops it instead.
3137
*/
3238
class FrameCallbacks {
3339
public:
@@ -42,6 +48,8 @@ class FrameCallbacks {
4248
static void RemoveIsolateEntries(v8::Isolate* isolate);
4349

4450
private:
51+
static void RequestAnimationFrame(const v8::FunctionCallbackInfo<v8::Value>& args);
52+
static void CancelAnimationFrame(const v8::FunctionCallbackInfo<v8::Value>& args);
4553
static void PostFrameCallback(const v8::FunctionCallbackInfo<v8::Value>& args);
4654
static void RemoveFrameCallback(const v8::FunctionCallbackInfo<v8::Value>& args);
4755
};

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL