blob: cbbe50fe337efe3858fe76bfd72ed9d32f417ba0 [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.
// @dart = 2.9
part of base;
typedef void AnimationCallback(num currentTime);
class CallbackData {
final AnimationCallback callback;
final num minTime;
int id;
static int _nextId;
bool ready(num time) => minTime == null || minTime <= time;
CallbackData(this.callback, this.minTime) {
// TODO(jacobr): static init needs cleanup, see http://b/4161827
if (_nextId == null) {
_nextId = 1;
}
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;
bool _isMobileSafari = false;
CssStyleDeclaration _safariHackStyle;
int _frameCount = 0;
AnimationScheduler() : _callbacks = new List<CallbackData>() {
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 = new 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 = null, num minTime = null]) {
final callbackData = new 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 = new 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 = new List<CallbackData>();
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';
}
}
}