OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 typedef void AnimationCallback(num currentTime); | |
6 | |
7 class CallbackData { | |
8 final AnimationCallback callback; | |
9 final num minTime; | |
10 int id; | |
11 | |
12 static int _nextId; | |
13 | |
14 bool ready(num time) => minTime === null || minTime <= time; | |
15 | |
16 CallbackData(this.callback, this.minTime) { | |
17 // TODO(jacobr): static init needs cleanup, see http://b/4161827 | |
18 if (_nextId === null) { | |
19 _nextId = 1; | |
20 } | |
21 id = _nextId++; | |
22 } | |
23 } | |
24 | |
25 /** | |
26 * Animation scheduler implementing the functionality provided by | |
27 * [:window.requestAnimationFrame:] for platforms that do not support it | |
28 * or support it badly. When multiple UI components are animating at once, | |
29 * this approach yields superior performance to calling setTimeout directly as | |
30 * all pieces of the UI will animate at the same time resulting in fewer | |
31 * layouts. | |
32 */ | |
33 // TODO(jacobr): use window.requestAnimationFrame when it is available and | |
34 // 60fps for the current browser. | |
35 class AnimationScheduler { | |
36 static final FRAMES_PER_SECOND = 60; | |
37 static final MS_PER_FRAME = 1000 ~/ FRAMES_PER_SECOND; | |
38 static final USE_INTERVALS = false; | |
39 | |
40 /** List of callbacks to be executed next animation frame. */ | |
41 List<CallbackData> _callbacks; | |
42 int _intervalId; | |
43 bool _isMobileSafari = false; | |
44 CSSStyleDeclaration _safariHackStyle; | |
45 int _frameCount = 0; | |
46 bool _webkitAnimationFrameMaybeAvailable = true; | |
47 | |
48 AnimationScheduler() | |
49 : _callbacks = new List<CallbackData>() { | |
50 if (_isMobileSafari) { | |
51 // TODO(jacobr): find a better workaround for the issue that 3d transforms | |
52 // sometimes don't render on iOS without forcing a layout. | |
53 final element = new Element.tag('div'); | |
54 document.body.nodes.add(element); | |
55 _safariHackStyle = element.style; | |
56 _safariHackStyle.position = 'absolute'; | |
57 } | |
58 } | |
59 | |
60 /** | |
61 * Cancel the pending callback matching the specified [id]. | |
62 * This is not heavily optimized as typically users don't cancel animation | |
63 * frames. | |
64 */ | |
65 void cancelRequestAnimationFrame(int id) { | |
66 _callbacks = _callbacks.filter((CallbackData e) => e.id != id); | |
67 } | |
68 | |
69 /** | |
70 * Schedule [callback] to execute at the next animation frame that occurs | |
71 * at or after [minTime]. If [minTime] is not specified, the first available | |
72 * animation frame is used. Returns an id that can be used to cancel the | |
73 * pending callback. | |
74 */ | |
75 int requestAnimationFrame(AnimationCallback callback, | |
76 [Element element = null, | |
77 num minTime = null]) { | |
78 final callbackData = new CallbackData(callback, minTime); | |
79 _requestAnimationFrameHelper(callbackData); | |
80 return callbackData.id; | |
81 } | |
82 | |
83 void _requestAnimationFrameHelper(CallbackData callbackData) { | |
84 _callbacks.add(callbackData); | |
85 if (_intervalId === null) { | |
86 _setupInterval(); | |
87 } | |
88 } | |
89 | |
90 void _setupInterval() { | |
91 // Assert that we never schedule multiple frames at once. | |
92 assert(_intervalId === null); | |
93 if (USE_INTERVALS) { | |
94 _intervalId = window.setInterval(_step, MS_PER_FRAME); | |
95 } else { | |
96 if (_webkitAnimationFrameMaybeAvailable) { | |
97 try { | |
98 // TODO(jacobr): passing in document should not be required. | |
99 _intervalId = window.webkitRequestAnimationFrame( | |
100 (int ignored) { _step(); }, document); | |
101 // TODO(jacobr) fix this odd type error. | |
102 } catch (var e) { | |
103 _webkitAnimationFrameMaybeAvailable = false; | |
104 } | |
105 } | |
106 if (!_webkitAnimationFrameMaybeAvailable) { | |
107 _intervalId = window.setTimeout(() { _step(); }, MS_PER_FRAME); | |
108 } | |
109 } | |
110 } | |
111 | |
112 void _step() { | |
113 if (_callbacks.isEmpty()) { | |
114 // Cancel the interval on the first frame where there aren't actually | |
115 // any available callbacks. | |
116 assert(_intervalId != null); | |
117 if (USE_INTERVALS) { | |
118 window.clearInterval(_intervalId); | |
119 } | |
120 _intervalId = null; | |
121 } else if (USE_INTERVALS == false) { | |
122 _intervalId = null; | |
123 _setupInterval(); | |
124 } | |
125 int numRemaining = 0; | |
126 int minTime = new Date.now().value + MS_PER_FRAME; | |
127 | |
128 int len = _callbacks.length; | |
129 for (final callback in _callbacks) { | |
130 if (!callback.ready(minTime)) { | |
131 numRemaining++; | |
132 } | |
133 } | |
134 | |
135 if (numRemaining == len) { | |
136 // TODO(jacobr): we could be more clever about this case if delayed | |
137 // requests really become the main use case... | |
138 return; | |
139 } | |
140 // Some callbacks need to be executed. | |
141 final currentCallbacks = _callbacks; | |
142 _callbacks = new List<CallbackData>(); | |
143 | |
144 for (final callbackData in currentCallbacks) { | |
145 if (callbackData.ready(minTime)) { | |
146 try { | |
147 (callbackData.callback)(minTime); | |
148 } catch (var e) { | |
149 final msg = e.toString(); | |
150 print('Suppressed exception ${msg} triggered by callback'); | |
151 } | |
152 } else { | |
153 _callbacks.add(callbackData); | |
154 } | |
155 } | |
156 | |
157 _frameCount++; | |
158 if (_isMobileSafari) { | |
159 // Hack to work around an iOS bug where sometimes animations do not | |
160 // render if only webkit transforms were modified. | |
161 // TODO(jacobr): find a cleaner workaround. | |
162 int offset = _frameCount % 2; | |
163 _safariHackStyle.left = '${offset}px'; | |
164 } | |
165 } | |
166 } | |
OLD | NEW |