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';
7library;
8
9import 'dart:async';
10
11import 'package:flutter/foundation.dart';
12
13import 'binding.dart';
14
15export 'dart:ui' show VoidCallback;
16
17export '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.
24typedef 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].
43abstract 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.
78class 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].
411class 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.
514class 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