1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'package:flutter/widgets.dart'; |
6 | /// @docImport 'package:flutter_test/flutter_test.dart'; |
7 | library; |
8 | |
9 | import 'dart:async'; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | |
13 | import 'binding.dart'; |
14 | |
15 | export 'dart:ui' show VoidCallback; |
16 | |
17 | export 'package:flutter/foundation.dart' show DiagnosticsNode; |
18 | |
19 | /// Signature for the callback passed to the [Ticker] class's constructor. |
20 | /// |
21 | /// The argument is the time elapsed from |
22 | /// the frame timestamp when the ticker was last started |
23 | /// to the current frame timestamp. |
24 | typedef TickerCallback = void Function(Duration elapsed); |
25 | |
26 | /// An interface implemented by classes that can vend [Ticker] objects. |
27 | /// |
28 | /// To obtain a [TickerProvider], consider mixing in either |
29 | /// [TickerProviderStateMixin] (which always works) |
30 | /// or [SingleTickerProviderStateMixin] (which is more efficient when it works) |
31 | /// to make a [State] subclass implement [TickerProvider]. |
32 | /// That [State] can then be passed to lower-level widgets |
33 | /// or other related objects. |
34 | /// This ensures the resulting [Ticker]s will only tick when that [State]'s |
35 | /// subtree is enabled, as defined by [TickerMode]. |
36 | /// |
37 | /// In widget tests, the [WidgetTester] object is also a [TickerProvider]. |
38 | /// |
39 | /// Tickers can be used by any object that wants to be notified whenever a frame |
40 | /// triggers, but are most commonly used indirectly via an |
41 | /// [AnimationController]. [AnimationController]s need a [TickerProvider] to |
42 | /// obtain their [Ticker]. |
43 | abstract class TickerProvider { |
44 | /// Abstract const constructor. This constructor enables subclasses to provide |
45 | /// const constructors so that they can be used in const expressions. |
46 | const TickerProvider(); |
47 | |
48 | /// Creates a ticker with the given callback. |
49 | /// |
50 | /// The kind of ticker provided depends on the kind of ticker provider. |
51 | @factory |
52 | Ticker createTicker(TickerCallback onTick); |
53 | } |
54 | |
55 | /// Calls its callback once per animation frame, when enabled. |
56 | /// |
57 | /// To obtain a ticker, consider [TickerProvider]. |
58 | /// |
59 | /// When created, a ticker is initially disabled. Call [start] to |
60 | /// enable the ticker. |
61 | /// |
62 | /// A [Ticker] can be silenced by setting [muted] to true. While silenced, time |
63 | /// still elapses, and [start] and [stop] can still be called, but no callbacks |
64 | /// are called. |
65 | /// |
66 | /// By convention, the [start] and [stop] methods are used by the ticker's |
67 | /// consumer (for example, an [AnimationController]), and the [muted] property |
68 | /// is controlled by the [TickerProvider] that created the ticker (for example, |
69 | /// a [State] that uses [TickerProviderStateMixin] to silence the ticker when |
70 | /// the state's subtree is disabled as defined by [TickerMode]). |
71 | /// |
72 | /// See also: |
73 | /// |
74 | /// * [TickerProvider], for obtaining a ticker. |
75 | /// * [SchedulerBinding.scheduleFrameCallback], which drives tickers. |
76 | // TODO(jacobr): make Ticker use Diagnosticable to simplify reporting errors |
77 | // related to a ticker. |
78 | class Ticker { |
79 | /// Creates a ticker that will call the provided callback once per frame while |
80 | /// running. |
81 | /// |
82 | /// An optional label can be provided for debugging purposes. That label |
83 | /// will appear in the [toString] output in debug builds. |
84 | Ticker(this._onTick, {this.debugLabel}) { |
85 | assert(() { |
86 | _debugCreationStack = StackTrace.current; |
87 | return true; |
88 | }()); |
89 | assert(debugMaybeDispatchCreated('scheduler' , 'Ticker' , this)); |
90 | } |
91 | |
92 | TickerFuture? _future; |
93 | |
94 | /// Whether this ticker has been silenced. |
95 | /// |
96 | /// While silenced, a ticker's clock can still run, but the callback will not |
97 | /// be called. |
98 | bool get muted => _muted; |
99 | bool _muted = false; |
100 | |
101 | /// When set to true, silences the ticker, so that it is no longer ticking. If |
102 | /// a tick is already scheduled, it will unschedule it. This will not |
103 | /// unschedule the next frame, though. |
104 | /// |
105 | /// When set to false, unsilences the ticker, potentially scheduling a frame |
106 | /// to handle the next tick. |
107 | /// |
108 | /// By convention, the [muted] property is controlled by the object that |
109 | /// created the [Ticker] (typically a [TickerProvider]), not the object that |
110 | /// listens to the ticker's ticks. |
111 | set muted(bool value) { |
112 | if (value == muted) { |
113 | return; |
114 | } |
115 | _muted = value; |
116 | if (value) { |
117 | unscheduleTick(); |
118 | } else if (shouldScheduleTick) { |
119 | scheduleTick(); |
120 | } |
121 | } |
122 | |
123 | /// Whether this [Ticker] has scheduled a call to call its callback |
124 | /// on the next frame. |
125 | /// |
126 | /// A ticker that is [muted] can be active (see [isActive]) yet not be |
127 | /// ticking. In that case, the ticker will not call its callback, and |
128 | /// [isTicking] will be false, but time will still be progressing. |
129 | /// |
130 | /// This will return false if the [SchedulerBinding.lifecycleState] is one |
131 | /// that indicates the application is not currently visible (e.g. if the |
132 | /// device's screen is turned off). |
133 | bool get isTicking { |
134 | if (_future == null) { |
135 | return false; |
136 | } |
137 | if (muted) { |
138 | return false; |
139 | } |
140 | if (SchedulerBinding.instance.framesEnabled) { |
141 | return true; |
142 | } |
143 | if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle) { |
144 | return true; |
145 | } // for example, we might be in a warm-up frame or forced frame |
146 | return false; |
147 | } |
148 | |
149 | /// Whether time is elapsing for this [Ticker]. Becomes true when [start] is |
150 | /// called and false when [stop] is called. |
151 | /// |
152 | /// A ticker can be active yet not be actually ticking (i.e. not be calling |
153 | /// the callback). To determine if a ticker is actually ticking, use |
154 | /// [isTicking]. |
155 | bool get isActive => _future != null; |
156 | |
157 | /// The frame timestamp when the ticker was last started, |
158 | /// as reported by [SchedulerBinding.currentFrameTimeStamp]. |
159 | Duration? _startTime; |
160 | |
161 | /// Starts the clock for this [Ticker]. If the ticker is not [muted], then this |
162 | /// also starts calling the ticker's callback once per animation frame. |
163 | /// |
164 | /// The returned future resolves once the ticker [stop]s ticking. If the |
165 | /// ticker is disposed, the future does not resolve. A derivative future is |
166 | /// available from the returned [TickerFuture] object that resolves with an |
167 | /// error in that case, via [TickerFuture.orCancel]. |
168 | /// |
169 | /// Calling this sets [isActive] to true. |
170 | /// |
171 | /// This method cannot be called while the ticker is active. To restart the |
172 | /// ticker, first [stop] it. |
173 | /// |
174 | /// By convention, this method is used by the object that receives the ticks |
175 | /// (as opposed to the [TickerProvider] which created the ticker). |
176 | TickerFuture start() { |
177 | assert(() { |
178 | if (isActive) { |
179 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
180 | ErrorSummary('A ticker was started twice.' ), |
181 | ErrorDescription( |
182 | 'A ticker that is already active cannot be started again without first stopping it.' , |
183 | ), |
184 | describeForError('The affected ticker was' ), |
185 | ]); |
186 | } |
187 | return true; |
188 | }()); |
189 | assert(_startTime == null); |
190 | _future = TickerFuture._(); |
191 | if (shouldScheduleTick) { |
192 | scheduleTick(); |
193 | } |
194 | if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index && |
195 | SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index) { |
196 | _startTime = SchedulerBinding.instance.currentFrameTimeStamp; |
197 | } |
198 | return _future!; |
199 | } |
200 | |
201 | /// Adds a debug representation of a [Ticker] optimized for including in error |
202 | /// messages. |
203 | DiagnosticsNode describeForError(String name) { |
204 | // TODO(jacobr): make this more structured. |
205 | return DiagnosticsProperty<Ticker>(name, this, description: toString(debugIncludeStack: true)); |
206 | } |
207 | |
208 | /// Stops calling this [Ticker]'s callback. |
209 | /// |
210 | /// If called with the `canceled` argument set to false (the default), causes |
211 | /// the future returned by [start] to resolve. If called with the `canceled` |
212 | /// argument set to true, the future does not resolve, and the future obtained |
213 | /// from [TickerFuture.orCancel], if any, resolves with a [TickerCanceled] |
214 | /// error. |
215 | /// |
216 | /// Calling this sets [isActive] to false. |
217 | /// |
218 | /// This method does nothing if called when the ticker is inactive. |
219 | /// |
220 | /// By convention, this method is used by the object that receives the ticks |
221 | /// (as opposed to the [TickerProvider] which created the ticker). |
222 | void stop({bool canceled = false}) { |
223 | if (!isActive) { |
224 | return; |
225 | } |
226 | |
227 | // We take the _future into a local variable so that isTicking is false |
228 | // when we actually complete the future (isTicking uses _future to |
229 | // determine its state). |
230 | final TickerFuture localFuture = _future!; |
231 | _future = null; |
232 | _startTime = null; |
233 | assert(!isActive); |
234 | |
235 | unscheduleTick(); |
236 | if (canceled) { |
237 | localFuture._cancel(this); |
238 | } else { |
239 | localFuture._complete(); |
240 | } |
241 | } |
242 | |
243 | final TickerCallback _onTick; |
244 | |
245 | int? _animationId; |
246 | |
247 | /// Whether this [Ticker] has already scheduled a frame callback. |
248 | @protected |
249 | bool get scheduled => _animationId != null; |
250 | |
251 | /// Whether a tick should be scheduled. |
252 | /// |
253 | /// If this is true, then calling [scheduleTick] should succeed. |
254 | /// |
255 | /// Reasons why a tick should not be scheduled include: |
256 | /// |
257 | /// * A tick has already been scheduled for the coming frame. |
258 | /// * The ticker is not active ([start] has not been called). |
259 | /// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]). |
260 | @protected |
261 | bool get shouldScheduleTick => !muted && isActive && !scheduled; |
262 | |
263 | void _tick(Duration timeStamp) { |
264 | assert(isTicking); |
265 | assert(scheduled); |
266 | _animationId = null; |
267 | |
268 | _startTime ??= timeStamp; |
269 | _onTick(timeStamp - _startTime!); |
270 | |
271 | // The onTick callback may have scheduled another tick already, for |
272 | // example by calling stop then start again. |
273 | if (shouldScheduleTick) { |
274 | scheduleTick(rescheduling: true); |
275 | } |
276 | } |
277 | |
278 | /// Schedules a tick for the next frame. |
279 | /// |
280 | /// This should only be called if [shouldScheduleTick] is true. |
281 | @protected |
282 | void scheduleTick({bool rescheduling = false}) { |
283 | assert(!scheduled); |
284 | assert(shouldScheduleTick); |
285 | _animationId = SchedulerBinding.instance.scheduleFrameCallback( |
286 | _tick, |
287 | rescheduling: rescheduling, |
288 | ); |
289 | } |
290 | |
291 | /// Cancels the frame callback that was requested by [scheduleTick], if any. |
292 | /// |
293 | /// Calling this method when no tick is [scheduled] is harmless. |
294 | /// |
295 | /// This method should not be called when [shouldScheduleTick] would return |
296 | /// true if no tick was scheduled. |
297 | @protected |
298 | void unscheduleTick() { |
299 | if (scheduled) { |
300 | SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId!); |
301 | _animationId = null; |
302 | } |
303 | assert(!shouldScheduleTick); |
304 | } |
305 | |
306 | /// Makes this [Ticker] take the state of another ticker, and disposes the |
307 | /// other ticker. |
308 | /// |
309 | /// This is useful if an object with a [Ticker] is given a new |
310 | /// [TickerProvider] but needs to maintain continuity. In particular, this |
311 | /// maintains the identity of the [TickerFuture] returned by the [start] |
312 | /// function of the original [Ticker] if the original ticker is active. |
313 | /// |
314 | /// This ticker must not be active when this method is called. |
315 | void absorbTicker(Ticker originalTicker) { |
316 | assert(!isActive); |
317 | assert(_future == null); |
318 | assert(_startTime == null); |
319 | assert(_animationId == null); |
320 | assert( |
321 | (originalTicker._future == null) == (originalTicker._startTime == null), |
322 | 'Cannot absorb Ticker after it has been disposed.' , |
323 | ); |
324 | if (originalTicker._future != null) { |
325 | _future = originalTicker._future; |
326 | _startTime = originalTicker._startTime; |
327 | if (shouldScheduleTick) { |
328 | scheduleTick(); |
329 | } |
330 | originalTicker._future = |
331 | null; // so that it doesn't get disposed when we dispose of originalTicker |
332 | originalTicker.unscheduleTick(); |
333 | } |
334 | originalTicker.dispose(); |
335 | } |
336 | |
337 | /// Release the resources used by this object. The object is no longer usable |
338 | /// after this method is called. |
339 | /// |
340 | /// It is legal to call this method while [isActive] is true, in which case: |
341 | /// |
342 | /// * The frame callback that was requested by [scheduleTick], if any, is |
343 | /// canceled. |
344 | /// * The future that was returned by [start] does not resolve. |
345 | /// * The future obtained from [TickerFuture.orCancel], if any, resolves |
346 | /// with a [TickerCanceled] error. |
347 | @mustCallSuper |
348 | void dispose() { |
349 | assert(debugMaybeDispatchDisposed(this)); |
350 | if (_future != null) { |
351 | final TickerFuture localFuture = _future!; |
352 | _future = null; |
353 | assert(!isActive); |
354 | unscheduleTick(); |
355 | localFuture._cancel(this); |
356 | } |
357 | assert(() { |
358 | // We intentionally don't null out _startTime. This means that if start() |
359 | // was ever called, the object is now in a bogus state. This weakly helps |
360 | // catch cases of use-after-dispose. |
361 | _startTime = Duration.zero; |
362 | return true; |
363 | }()); |
364 | } |
365 | |
366 | /// An optional label can be provided for debugging purposes. |
367 | /// |
368 | /// This label will appear in the [toString] output in debug builds. |
369 | final String? debugLabel; |
370 | late StackTrace _debugCreationStack; |
371 | |
372 | @override |
373 | String toString({bool debugIncludeStack = false}) { |
374 | final StringBuffer buffer = StringBuffer(); |
375 | buffer.write(' ${objectRuntimeType(this, 'Ticker' )}(' ); |
376 | assert(() { |
377 | buffer.write(debugLabel ?? '' ); |
378 | return true; |
379 | }()); |
380 | buffer.write(')' ); |
381 | assert(() { |
382 | if (debugIncludeStack) { |
383 | buffer.writeln(); |
384 | buffer.writeln('The stack trace when the $runtimeType was actually created was:' ); |
385 | FlutterError.defaultStackFilter( |
386 | _debugCreationStack.toString().trimRight().split('\n' ), |
387 | ).forEach(buffer.writeln); |
388 | } |
389 | return true; |
390 | }()); |
391 | return buffer.toString(); |
392 | } |
393 | } |
394 | |
395 | /// An object representing an ongoing [Ticker] sequence. |
396 | /// |
397 | /// The [Ticker.start] method returns a [TickerFuture]. The [TickerFuture] will |
398 | /// complete successfully if the [Ticker] is stopped using [Ticker.stop] with |
399 | /// the `canceled` argument set to false (the default). |
400 | /// |
401 | /// If the [Ticker] is disposed without being stopped, or if it is stopped with |
402 | /// `canceled` set to true, then this Future will never complete. |
403 | /// |
404 | /// This class works like a normal [Future], but has an additional property, |
405 | /// [orCancel], which returns a derivative [Future] that completes with an error |
406 | /// if the [Ticker] that returned the [TickerFuture] was stopped with `canceled` |
407 | /// set to true, or if it was disposed without being stopped. |
408 | /// |
409 | /// To run a callback when either this future resolves or when the ticker is |
410 | /// canceled, use [whenCompleteOrCancel]. |
411 | class TickerFuture implements Future<void> { |
412 | TickerFuture._(); |
413 | |
414 | /// Creates a [TickerFuture] instance that represents an already-complete |
415 | /// [Ticker] sequence. |
416 | /// |
417 | /// This is useful for implementing objects that normally defer to a [Ticker] |
418 | /// but sometimes can skip the ticker because the animation is of zero |
419 | /// duration, but which still need to represent the completed animation in the |
420 | /// form of a [TickerFuture]. |
421 | TickerFuture.complete() { |
422 | _complete(); |
423 | } |
424 | |
425 | final Completer<void> _primaryCompleter = Completer<void>(); |
426 | Completer<void>? _secondaryCompleter; |
427 | bool? _completed; // null means unresolved, true means complete, false means canceled |
428 | |
429 | void _complete() { |
430 | assert(_completed == null); |
431 | _completed = true; |
432 | _primaryCompleter.complete(); |
433 | _secondaryCompleter?.complete(); |
434 | } |
435 | |
436 | void _cancel(Ticker ticker) { |
437 | assert(_completed == null); |
438 | _completed = false; |
439 | _secondaryCompleter?.completeError(TickerCanceled(ticker)); |
440 | } |
441 | |
442 | /// Calls `callback` either when this future resolves or when the ticker is |
443 | /// canceled. |
444 | /// |
445 | /// Calling this method registers an exception handler for the [orCancel] |
446 | /// future, so even if the [orCancel] property is accessed, canceling the |
447 | /// ticker will not cause an uncaught exception in the current zone. |
448 | void whenCompleteOrCancel(VoidCallback callback) { |
449 | void thunk(dynamic value) { |
450 | callback(); |
451 | } |
452 | |
453 | orCancel.then<void>(thunk, onError: thunk); |
454 | } |
455 | |
456 | /// A future that resolves when this future resolves or throws when the ticker |
457 | /// is canceled. |
458 | /// |
459 | /// If this property is never accessed, then canceling the ticker does not |
460 | /// throw any exceptions. Once this property is accessed, though, if the |
461 | /// corresponding ticker is canceled, then the [Future] returned by this |
462 | /// getter will complete with an error, and if that error is not caught, there |
463 | /// will be an uncaught exception in the current zone. |
464 | Future<void> get orCancel { |
465 | if (_secondaryCompleter == null) { |
466 | _secondaryCompleter = Completer<void>(); |
467 | if (_completed != null) { |
468 | if (_completed!) { |
469 | _secondaryCompleter!.complete(); |
470 | } else { |
471 | _secondaryCompleter!.completeError(const TickerCanceled()); |
472 | } |
473 | } |
474 | } |
475 | return _secondaryCompleter!.future; |
476 | } |
477 | |
478 | @override |
479 | Stream<void> asStream() { |
480 | return _primaryCompleter.future.asStream(); |
481 | } |
482 | |
483 | @override |
484 | Future<void> catchError(Function onError, {bool Function(Object)? test}) { |
485 | return _primaryCompleter.future.catchError(onError, test: test); |
486 | } |
487 | |
488 | @override |
489 | Future<R> then<R>(FutureOr<R> Function(void value) onValue, {Function? onError}) { |
490 | return _primaryCompleter.future.then<R>(onValue, onError: onError); |
491 | } |
492 | |
493 | @override |
494 | Future<void> timeout(Duration timeLimit, {FutureOr<void> Function()? onTimeout}) { |
495 | return _primaryCompleter.future.timeout(timeLimit, onTimeout: onTimeout); |
496 | } |
497 | |
498 | @override |
499 | Future<void> whenComplete(dynamic Function() action) { |
500 | return _primaryCompleter.future.whenComplete(action); |
501 | } |
502 | |
503 | @override |
504 | String toString() => |
505 | ' ${describeIdentity(this)}( ${_completed == null |
506 | ? "active" |
507 | : _completed! |
508 | ? "complete" |
509 | : "canceled" })' ; |
510 | } |
511 | |
512 | /// Exception thrown by [Ticker] objects on the [TickerFuture.orCancel] future |
513 | /// when the ticker is canceled. |
514 | class TickerCanceled implements Exception { |
515 | /// Creates a canceled-ticker exception. |
516 | const TickerCanceled([this.ticker]); |
517 | |
518 | /// Reference to the [Ticker] object that was canceled. |
519 | /// |
520 | /// This may be null in the case that the [Future] created for |
521 | /// [TickerFuture.orCancel] was created after the ticker was canceled. |
522 | final Ticker? ticker; |
523 | |
524 | @override |
525 | String toString() { |
526 | if (ticker != null) { |
527 | return 'This ticker was canceled: $ticker' ; |
528 | } |
529 | return 'The ticker was canceled before the "orCancel" property was first used.' ; |
530 | } |
531 | } |
532 | |