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