blob: c273c6c5b7a5f679e7d6343af1b496f684a3717e [file] [log] [blame]
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of base;
typedef AnimationCallback = void Function(num currentTime);
class CallbackData {
final AnimationCallback callback;
final num minTime;
final int id;
static int _nextId = 1;
bool ready(num time) => minTime <= time;
CallbackData(this.callback, this.minTime) : id = _nextId++;
}
/// Animation scheduler implementing the functionality provided by
/// [:window.requestAnimationFrame:] for platforms that do not support it
/// or support it badly. When multiple UI components are animating at once,
/// this approach yields superior performance to calling setTimeout/Timer
/// directly as all pieces of the UI will animate at the same time resulting in
/// fewer layouts.
// TODO(jacobr): use window.requestAnimationFrame when it is available and
// 60fps for the current browser.
class AnimationScheduler {
static const FRAMES_PER_SECOND = 60;
static const MS_PER_FRAME = 1000 ~/ FRAMES_PER_SECOND;
/// List of callbacks to be executed next animation frame. */
List<CallbackData> _callbacks;
final bool _isMobileSafari = false;
late CssStyleDeclaration _safariHackStyle;
int _frameCount = 0;
AnimationScheduler() : _callbacks = List<CallbackData>.empty() {
if (_isMobileSafari) {
// TODO(jacobr): find a better workaround for the issue that 3d transforms
// sometimes don't render on iOS without forcing a layout.
final element = Element.tag('div');
document.body!.nodes.add(element);
_safariHackStyle = element.style;
_safariHackStyle.position = 'absolute';
}
}
/// Cancel the pending callback matching the specified [id].
/// This is not heavily optimized as typically users don't cancel animation
/// frames.
void cancelRequestAnimationFrame(int id) {
_callbacks = _callbacks.where((CallbackData e) => e.id != id).toList();
}
/// Schedule [callback] to execute at the next animation frame that occurs
/// at or after [minTime]. If [minTime] is not specified, the first available
/// animation frame is used. Returns an id that can be used to cancel the
/// pending callback.
int requestAnimationFrame(AnimationCallback callback,
[Element? element, num? minTime]) {
final callbackData = CallbackData(callback, minTime!);
_requestAnimationFrameHelper(callbackData);
return callbackData.id;
}
void _requestAnimationFrameHelper(CallbackData callbackData) {
_callbacks.add(callbackData);
_setupInterval();
}
void _setupInterval() {
window.requestAnimationFrame((num ignored) {
_step();
});
}
void _step() {
if (_callbacks.isEmpty) {
// Cancel the interval on the first frame where there aren't actually
// any available callbacks.
} else {
_setupInterval();
}
int numRemaining = 0;
int minTime = DateTime.now().millisecondsSinceEpoch + MS_PER_FRAME;
int len = _callbacks.length;
for (final callback in _callbacks) {
if (!callback.ready(minTime)) {
numRemaining++;
}
}
if (numRemaining == len) {
// TODO(jacobr): we could be more clever about this case if delayed
// requests really become the main use case...
return;
}
// Some callbacks need to be executed.
final currentCallbacks = _callbacks;
_callbacks = List<CallbackData>.empty();
for (final callbackData in currentCallbacks) {
if (callbackData.ready(minTime)) {
try {
(callbackData.callback)(minTime);
} catch (e) {
final msg = e.toString();
print('Suppressed exception $msg triggered by callback');
}
} else {
_callbacks.add(callbackData);
}
}
_frameCount++;
if (_isMobileSafari) {
// Hack to work around an iOS bug where sometimes animations do not
// render if only webkit transforms were modified.
// TODO(jacobr): find a cleaner workaround.
int offset = _frameCount % 2;
_safariHackStyle.left = '${offset}px';
}
}
}