// 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 _callbacks; bool _isMobileSafari = false; CssStyleDeclaration _safariHackStyle; int _frameCount = 0; AnimationScheduler() : _callbacks = new List() { 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(); 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'; } } }