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 | import 'dart:async'; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | |
9 | import 'binding.dart'; |
10 | |
11 | export 'dart:ui' show VoidCallback; |
12 | |
13 | export '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. |
19 | typedef 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. |
34 | abstract 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]. |
63 | class 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]. |
396 | class 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. |
493 | class 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 | |