| // 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'; |
| } |
| } |
| } |