ticker.dart 16.3 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium 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';

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

9
import 'binding.dart';
10

11
/// Signature for the callback passed to the [Ticker] class's constructor.
12 13
///
/// The argument is the time that the object had spent enabled so far
14
/// at the time of the callback being called.
15
typedef void TickerCallback(Duration elapsed);
16

17
/// An interface implemented by classes that can vend [Ticker] objects.
18 19 20 21 22 23 24 25 26 27 28 29
///
/// 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.
30 31 32 33 34 35 36 37 38 39 40
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);
}

Florian Loitsch's avatar
Florian Loitsch committed
41
/// Calls its callback once per animation frame.
42 43 44 45
///
/// When created, a ticker is initially disabled. Call [start] to
/// enable the ticker.
///
46 47 48 49
/// 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.
///
50 51 52 53
/// 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.
///
54 55
/// Tickers are driven by the [SchedulerBinding]. See
/// [SchedulerBinding.scheduleFrameCallback].
56
class Ticker {
57 58
  /// Creates a ticker that will call the provided callback once per frame while
  /// running.
59 60 61 62 63 64 65
  ///
  /// 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;
66
    }());
67
  }
68

69
  TickerFuture _future;
70 71 72 73 74 75 76 77 78 79 80 81 82

  /// 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.
83 84 85 86
  ///
  /// 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.
87 88 89 90 91 92 93 94 95 96
  set muted(bool value) {
    if (value == muted)
      return;
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if (shouldScheduleTick) {
      scheduleTick();
    }
  }
97

98
  /// Whether this [Ticker] has scheduled a call to call its callback
99
  /// on the next frame.
100 101 102 103
  ///
  /// 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.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  ///
  /// 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;
  }
119

120
  /// Whether time is elapsing for this [Ticker]. Becomes true when [start] is
121
  /// called and false when [stop] is called.
122
  ///
123 124 125
  /// 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].
126
  bool get isActive => _future != null;
127 128 129

  Duration _startTime;

130
  /// Starts the clock for this [Ticker]. If the ticker is not [muted], then this
131 132
  /// also starts calling the ticker's callback once per animation frame.
  ///
133 134 135 136
  /// 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].
137 138 139 140 141
  ///
  /// Calling this sets [isActive] to true.
  ///
  /// This method cannot be called while the ticker is active. To restart the
  /// ticker, first [stop] it.
142 143 144
  ///
  /// By convention, this method is used by the object that receives the ticks
  /// (as opposed to the [TickerProvider] which created the ticker).
145
  TickerFuture start() {
146
    assert(() {
147
      if (isActive) {
148 149 150
        throw new FlutterError(
          'A ticker was started twice.\n'
          'A ticker that is already active cannot be started again without first stopping it.\n'
151
          'The affected ticker was: ${ toString(debugIncludeStack: true) }'
152 153 154
        );
      }
      return true;
155
    }());
156
    assert(_startTime == null);
157
    _future = new TickerFuture._();
158 159 160 161
    if (shouldScheduleTick)
      scheduleTick();
    if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
        SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
162
      _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
163
    return _future;
164 165
  }

166
  /// Stops calling this [Ticker]'s callback.
167
  ///
168 169 170 171 172
  /// 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.
173 174 175 176
  ///
  /// Calling this sets [isActive] to false.
  ///
  /// This method does nothing if called when the ticker is inactive.
177 178 179
  ///
  /// By convention, this method is used by the object that receives the ticks
  /// (as opposed to the [TickerProvider] which created the ticker).
180
  void stop({ bool canceled: false }) {
181
    if (!isActive)
182 183
      return;

184 185
    // We take the _future into a local variable so that isTicking is false
    // when we actually complete the future (isTicking uses _future to
186
    // determine its state).
187 188
    final TickerFuture localFuture = _future;
    _future = null;
189
    _startTime = null;
190
    assert(!isActive);
191 192

    unscheduleTick();
193 194 195 196 197
    if (canceled) {
      localFuture._cancel(this);
    } else {
      localFuture._complete();
    }
198 199
  }

200 201 202 203 204

  final TickerCallback _onTick;

  int _animationId;

205
  /// Whether this [Ticker] has already scheduled a frame callback.
206 207 208 209 210 211 212 213 214 215 216 217
  @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]).
218
  @protected
219 220
  bool get shouldScheduleTick => isTicking && !scheduled;

221
  void _tick(Duration timeStamp) {
222
    assert(isTicking);
223
    assert(scheduled);
224 225
    _animationId = null;

226
    _startTime ??= timeStamp;
227 228

    _onTick(timeStamp - _startTime);
229

230 231 232 233
    // The onTick callback may have scheduled another tick already, for
    // example by calling stop then start again.
    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
234 235
  }

236 237 238 239 240
  /// Schedules a tick for the next frame.
  ///
  /// This should only be called if [shouldScheduleTick] is true.
  @protected
  void scheduleTick({ bool rescheduling: false }) {
241
    assert(isTicking);
242 243
    assert(!scheduled);
    assert(shouldScheduleTick);
244
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
245
  }
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

  /// 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);
  }

262 263
  /// Makes this [Ticker] take the state of another ticker, and disposes the
  /// other ticker.
264 265 266
  ///
  /// This is useful if an object with a [Ticker] is given a new
  /// [TickerProvider] but needs to maintain continuity. In particular, this
267 268
  /// maintains the identity of the [TickerFuture] returned by the [start]
  /// function of the original [Ticker] if the original ticker is active.
269 270 271
  ///
  /// This ticker must not be active when this method is called.
  void absorbTicker(Ticker originalTicker) {
272
    assert(!isActive);
273
    assert(_future == null);
274 275
    assert(_startTime == null);
    assert(_animationId == null);
276 277 278 279 280 281 282 283 284
    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();
    }
285 286 287 288 289 290 291
    originalTicker.dispose();
  }

  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
  @mustCallSuper
  void dispose() {
292 293 294 295 296 297 298 299 300 301 302 303 304
    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 = const Duration();
      return true;
305
    }());
306 307
  }

308 309 310
  /// An optional label can be provided for debugging purposes.
  ///
  /// This label will appear in the [toString] output in debug builds.
311 312 313 314 315 316 317 318 319 320
  final String debugLabel;
  StackTrace _debugCreationStack;

  @override
  String toString({ bool debugIncludeStack: false }) {
    final StringBuffer buffer = new StringBuffer();
    buffer.write('$runtimeType(');
    assert(() {
      buffer.write(debugLabel ?? '');
      return true;
321
    }());
322 323 324 325 326 327 328 329
    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;
330
    }());
331 332
    return buffer.toString();
  }
333
}
334 335 336 337 338 339 340 341 342 343 344 345 346 347

/// 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.
348
///
349
/// To run a callback when either this future resolves or when the ticker is
350
/// canceled, use [whenCompleteOrCancel].
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
class TickerFuture implements Future<Null> {
  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<Null> _primaryCompleter = new Completer<Null>();
  Completer<Null> _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(new TickerCanceled(ticker));
  }

382 383
  /// Calls `callback` either when this future resolves or when the ticker is
  /// canceled.
384 385 386 387
  ///
  /// 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.
388 389 390 391 392 393 394 395
  void whenCompleteOrCancel(VoidCallback callback) {
    Null thunk(dynamic value) {
      callback();
      return null;
    }
    orCancel.then(thunk, onError: thunk);
  }

396 397
  /// A future that resolves when this future resolves or throws when the ticker
  /// is canceled.
398 399 400 401 402 403
  ///
  /// 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.
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
  Future<Null> get orCancel {
    if (_secondaryCompleter == null) {
      _secondaryCompleter = new Completer<Null>();
      if (_completed != null) {
        if (_completed) {
          _secondaryCompleter.complete(null);
        } else {
          _secondaryCompleter.completeError(const TickerCanceled());
        }
      }
    }
    return _secondaryCompleter.future;
  }

  @override
  Stream<Null> asStream() {
    return _primaryCompleter.future.asStream();
  }

  @override
  Future<Null> catchError(Function onError, { bool test(dynamic error) }) {
    return _primaryCompleter.future.catchError(onError, test: test);
  }

  @override
  Future<E> then<E>(dynamic f(Null value), { Function onError }) {
    return _primaryCompleter.future.then<E>(f, onError: onError);
  }

  @override
  Future<Null> timeout(Duration timeLimit, { dynamic onTimeout() }) {
    return _primaryCompleter.future.timeout(timeLimit, onTimeout: onTimeout);
  }

  @override
  Future<Null> whenComplete(dynamic action()) {
    return _primaryCompleter.future.whenComplete(action);
  }

  @override
444
  String toString() => '${describeIdentity(this)}(${ _completed == null ? "active" : _completed ? "complete" : "canceled" })';
445 446 447 448 449 450 451 452 453 454 455
}

/// 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
456
  /// [TickerFuture.orCancel] was created after the ticker was canceled.
457 458 459 460 461 462 463 464 465
  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.';
  }
}