Files
sdk/client/base/AnimationScheduler.dart
T
dgrove@google.com 4bc64b8c2b Initial checkin.
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@7 260f80e4-7a28-3924-810f-c04153c831b5
2011-10-05 04:52:33 +00:00

167 lines
5.2 KiB
Dart

// 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.
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 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 final FRAMES_PER_SECOND = 60;
static final MS_PER_FRAME = 1000 ~/ FRAMES_PER_SECOND;
static final USE_INTERVALS = false;
/** List of callbacks to be executed next animation frame. */
List<CallbackData> _callbacks;
int _intervalId;
bool _isMobileSafari = false;
Css _safariHackCss;
int _frameCount = 0;
bool _webkitAnimationFrameMaybeAvailable = true;
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);
_safariHackCss = new Css(element.style);
_safariHackCss.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.filter((CallbackData e) => e.id != id);
}
/**
* 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);
if (_intervalId === null) {
_setupInterval();
}
}
void _setupInterval() {
// Assert that we never schedule multiple frames at once.
assert(_intervalId === null);
if (USE_INTERVALS) {
_intervalId = window.setInterval(_step, MS_PER_FRAME);
} else {
if (_webkitAnimationFrameMaybeAvailable) {
try {
// TODO(jacobr): passing in document should not be required.
_intervalId = window.webkitRequestAnimationFrame(
(int ignored) { _step(); }, document);
// TODO(jacobr) fix this odd type error.
} catch (var e) {
_webkitAnimationFrameMaybeAvailable = false;
}
}
if (!_webkitAnimationFrameMaybeAvailable) {
_intervalId = window.setTimeout(() { _step(); }, MS_PER_FRAME);
}
}
}
void _step() {
if (_callbacks.isEmpty()) {
// Cancel the interval on the first frame where there aren't actually
// any available callbacks.
assert(_intervalId != null);
if (USE_INTERVALS) {
window.clearInterval(_intervalId);
}
_intervalId = null;
} else if (USE_INTERVALS == false) {
_intervalId = null;
_setupInterval();
}
int numRemaining = 0;
int minTime = new DateTime.now().value + 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 (var 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;
_safariHackCss.left = '${offset}px';
}
}
}