| // 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. |
| Ticker createTicker(TickerCallback onTick); |
| } |
| |
| // TODO(jacobr): make Ticker use DiagnosticableMixin 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 [Scheduler.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; |
| 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(null); |
| _secondaryCompleter?.complete(null); |
| } |
| |
| 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 test(dynamic error) }) { |
| 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, { dynamic 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.'; |
| } |
| } |