ticker.dart 17.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7 8
import 'package:flutter/foundation.dart';

9
import 'binding.dart';
10

11 12 13
export 'dart:ui' show VoidCallback;

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 24 25 26 27 28 29 30 31 32 33
///
/// 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.
34 35 36 37 38 39 40 41
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.
42
  @factory
43 44 45
  Ticker createTicker(TickerCallback onTick);
}

46
// TODO(jacobr): make Ticker use Diagnosticable to simplify reporting errors
47
// related to a ticker.
Florian Loitsch's avatar
Florian Loitsch committed
48
/// Calls its callback once per animation frame.
49 50 51 52
///
/// When created, a ticker is initially disabled. Call [start] to
/// enable the ticker.
///
53 54 55 56
/// 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.
///
57 58 59 60
/// 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.
///
61 62
/// Tickers are driven by the [SchedulerBinding]. See
/// [SchedulerBinding.scheduleFrameCallback].
63
class Ticker {
64 65
  /// Creates a ticker that will call the provided callback once per frame while
  /// running.
66 67 68 69 70 71 72
  ///
  /// 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;
73
    }());
74
  }
75

76
  TickerFuture? _future;
77 78 79 80 81 82 83 84 85 86 87 88 89

  /// 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.
90 91 92 93
  ///
  /// 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.
94
  set muted(bool value) {
95
    if (value == muted) {
96
      return;
97
    }
98 99 100 101 102 103 104
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if (shouldScheduleTick) {
      scheduleTick();
    }
  }
105

106
  /// Whether this [Ticker] has scheduled a call to call its callback
107
  /// on the next frame.
108 109 110 111
  ///
  /// 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.
112
  ///
113 114 115
  /// 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).
116
  bool get isTicking {
117
    if (_future == null) {
118
      return false;
119 120
    }
    if (muted) {
121
      return false;
122 123
    }
    if (SchedulerBinding.instance.framesEnabled) {
124
      return true;
125 126 127 128
    }
    if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle) {
      return true;
    } // for example, we might be in a warm-up frame or forced frame
129 130
    return false;
  }
131

132
  /// Whether time is elapsing for this [Ticker]. Becomes true when [start] is
133
  /// called and false when [stop] is called.
134
  ///
135 136 137
  /// 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].
138
  bool get isActive => _future != null;
139

140
  Duration? _startTime;
141

142
  /// Starts the clock for this [Ticker]. If the ticker is not [muted], then this
143 144
  /// also starts calling the ticker's callback once per animation frame.
  ///
145 146 147 148
  /// 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].
149 150 151 152 153
  ///
  /// Calling this sets [isActive] to true.
  ///
  /// This method cannot be called while the ticker is active. To restart the
  /// ticker, first [stop] it.
154 155 156
  ///
  /// By convention, this method is used by the object that receives the ticks
  /// (as opposed to the [TickerProvider] which created the ticker).
157
  TickerFuture start() {
158
    assert(() {
159
      if (isActive) {
160 161 162 163 164
        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'),
        ]);
165 166
      }
      return true;
167
    }());
168
    assert(_startTime == null);
169
    _future = TickerFuture._();
170
    if (shouldScheduleTick) {
171
      scheduleTick();
172
    }
173
    if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
174
        SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index) {
175
      _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
176
    }
177
    return _future!;
178 179
  }

180 181 182 183 184 185 186
  /// 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));
  }

187
  /// Stops calling this [Ticker]'s callback.
188
  ///
189 190 191 192 193
  /// 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.
194 195 196 197
  ///
  /// Calling this sets [isActive] to false.
  ///
  /// This method does nothing if called when the ticker is inactive.
198 199 200
  ///
  /// By convention, this method is used by the object that receives the ticks
  /// (as opposed to the [TickerProvider] which created the ticker).
201
  void stop({ bool canceled = false }) {
202
    if (!isActive) {
203
      return;
204
    }
205

206 207
    // We take the _future into a local variable so that isTicking is false
    // when we actually complete the future (isTicking uses _future to
208
    // determine its state).
209
    final TickerFuture localFuture = _future!;
210
    _future = null;
211
    _startTime = null;
212
    assert(!isActive);
213 214

    unscheduleTick();
215 216 217 218 219
    if (canceled) {
      localFuture._cancel(this);
    } else {
      localFuture._complete();
    }
220 221
  }

222 223 224

  final TickerCallback _onTick;

225
  int? _animationId;
226

227
  /// Whether this [Ticker] has already scheduled a frame callback.
228 229 230 231 232 233 234 235 236 237 238 239
  @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]).
240
  @protected
241
  bool get shouldScheduleTick => !muted && isActive && !scheduled;
242

243
  void _tick(Duration timeStamp) {
244
    assert(isTicking);
245
    assert(scheduled);
246 247
    _animationId = null;

248
    _startTime ??= timeStamp;
249
    _onTick(timeStamp - _startTime!);
250

251 252
    // The onTick callback may have scheduled another tick already, for
    // example by calling stop then start again.
253
    if (shouldScheduleTick) {
254
      scheduleTick(rescheduling: true);
255
    }
256 257
  }

258 259 260 261
  /// Schedules a tick for the next frame.
  ///
  /// This should only be called if [shouldScheduleTick] is true.
  @protected
262
  void scheduleTick({ bool rescheduling = false }) {
263 264
    assert(!scheduled);
    assert(shouldScheduleTick);
265
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
266
  }
267 268 269 270 271 272 273 274 275 276

  /// 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) {
277
      SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId!);
278 279 280 281 282
      _animationId = null;
    }
    assert(!shouldScheduleTick);
  }

283 284
  /// Makes this [Ticker] take the state of another ticker, and disposes the
  /// other ticker.
285 286 287
  ///
  /// This is useful if an object with a [Ticker] is given a new
  /// [TickerProvider] but needs to maintain continuity. In particular, this
288 289
  /// maintains the identity of the [TickerFuture] returned by the [start]
  /// function of the original [Ticker] if the original ticker is active.
290 291 292
  ///
  /// This ticker must not be active when this method is called.
  void absorbTicker(Ticker originalTicker) {
293
    assert(!isActive);
294
    assert(_future == null);
295 296
    assert(_startTime == null);
    assert(_animationId == null);
297 298 299 300
    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;
301
      if (shouldScheduleTick) {
302
        scheduleTick();
303
      }
304 305 306
      originalTicker._future = null; // so that it doesn't get disposed when we dispose of originalTicker
      originalTicker.unscheduleTick();
    }
307 308 309 310 311
    originalTicker.dispose();
  }

  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
312 313 314 315 316 317 318 319
  ///
  /// It is legal to call this method while [isActive] is true, in which case:
  ///
  ///  * The frame callback that was requested by [scheduleTick], if any, is
  ///    canceled.
  ///  * The future that was returned by [start] does not resolve.
  ///  * The future obtained from [TickerFuture.orCancel], if any, resolves
  ///    with a [TickerCanceled] error.
320 321
  @mustCallSuper
  void dispose() {
322
    if (_future != null) {
323
      final TickerFuture localFuture = _future!;
324 325 326 327 328 329 330 331 332
      _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.
333
      _startTime = Duration.zero;
334
      return true;
335
    }());
336 337
  }

338 339 340
  /// An optional label can be provided for debugging purposes.
  ///
  /// This label will appear in the [toString] output in debug builds.
341 342
  final String? debugLabel;
  late StackTrace _debugCreationStack;
343 344

  @override
345
  String toString({ bool debugIncludeStack = false }) {
346
    final StringBuffer buffer = StringBuffer();
347
    buffer.write('${objectRuntimeType(this, 'Ticker')}(');
348 349 350
    assert(() {
      buffer.write(debugLabel ?? '');
      return true;
351
    }());
352 353 354 355 356 357 358 359
    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;
360
    }());
361 362
    return buffer.toString();
  }
363
}
364 365 366 367 368 369 370 371 372 373 374 375 376 377

/// 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.
378
///
379
/// To run a callback when either this future resolves or when the ticker is
380
/// canceled, use [whenCompleteOrCancel].
381
class TickerFuture implements Future<void> {
382 383 384 385 386 387 388 389 390 391 392 393 394
  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();
  }

395
  final Completer<void> _primaryCompleter = Completer<void>();
396 397
  Completer<void>? _secondaryCompleter;
  bool? _completed; // null means unresolved, true means complete, false means canceled
398 399 400 401

  void _complete() {
    assert(_completed == null);
    _completed = true;
402 403
    _primaryCompleter.complete();
    _secondaryCompleter?.complete();
404 405 406 407 408
  }

  void _cancel(Ticker ticker) {
    assert(_completed == null);
    _completed = false;
409
    _secondaryCompleter?.completeError(TickerCanceled(ticker));
410 411
  }

412 413
  /// Calls `callback` either when this future resolves or when the ticker is
  /// canceled.
414 415 416 417
  ///
  /// 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.
418
  void whenCompleteOrCancel(VoidCallback callback) {
419
    void thunk(dynamic value) {
420 421
      callback();
    }
422
    orCancel.then<void>(thunk, onError: thunk);
423 424
  }

425 426
  /// A future that resolves when this future resolves or throws when the ticker
  /// is canceled.
427 428 429 430 431 432
  ///
  /// 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.
433
  Future<void> get orCancel {
434
    if (_secondaryCompleter == null) {
435
      _secondaryCompleter = Completer<void>();
436
      if (_completed != null) {
437 438
        if (_completed!) {
          _secondaryCompleter!.complete();
439
        } else {
440
          _secondaryCompleter!.completeError(const TickerCanceled());
441 442 443
        }
      }
    }
444
    return _secondaryCompleter!.future;
445 446 447
  }

  @override
448
  Stream<void> asStream() {
449 450 451 452
    return _primaryCompleter.future.asStream();
  }

  @override
453
  Future<void> catchError(Function onError, { bool Function(Object)? test }) {
454 455 456 457
    return _primaryCompleter.future.catchError(onError, test: test);
  }

  @override
458
  Future<R> then<R>(FutureOr<R> Function(void value) onValue, { Function? onError }) {
459
    return _primaryCompleter.future.then<R>(onValue, onError: onError);
460 461 462
  }

  @override
463
  Future<void> timeout(Duration timeLimit, { FutureOr<void> Function()? onTimeout }) {
464 465 466 467
    return _primaryCompleter.future.timeout(timeLimit, onTimeout: onTimeout);
  }

  @override
468
  Future<void> whenComplete(dynamic Function() action) {
469 470 471 472
    return _primaryCompleter.future.whenComplete(action);
  }

  @override
473
  String toString() => '${describeIdentity(this)}(${ _completed == null ? "active" : _completed! ? "complete" : "canceled" })';
474 475 476 477 478 479 480 481 482 483 484
}

/// 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
485
  /// [TickerFuture.orCancel] was created after the ticker was canceled.
486
  final Ticker? ticker;
487 488 489

  @override
  String toString() {
490
    if (ticker != null) {
491
      return 'This ticker was canceled: $ticker';
492
    }
493 494
    return 'The ticker was canceled before the "orCancel" property was first used.';
  }
495
}