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