animation_controller.dart 23.1 KB
Newer Older
1 2 3 4 5
// Copyright 2016 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';
6
import 'dart:ui' as ui show lerpDouble;
7

8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/physics.dart';
10
import 'package:flutter/scheduler.dart';
11 12

import 'animation.dart';
13
import 'curves.dart';
14 15
import 'listener_helpers.dart';

16 17
export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled;

18 19 20
// Examples can assume:
// AnimationController _controller;

Adam Barth's avatar
Adam Barth committed
21 22 23 24 25 26
/// The direction in which an animation is running.
enum _AnimationDirection {
  /// The animation is running from beginning to end.
  forward,

  /// The animation is running backwards, from end to beginning.
27
  reverse,
Adam Barth's avatar
Adam Barth committed
28 29
}

30 31
final SpringDescription _kFlingSpringDescription = new SpringDescription.withDampingRatio(
  mass: 1.0,
32
  stiffness: 500.0,
33 34 35 36
  ratio: 1.0,
);

const Tolerance _kFlingTolerance = const Tolerance(
37
  velocity: double.infinity,
38 39 40
  distance: 0.01,
);

41 42
/// A controller for an animation.
///
43 44 45 46 47 48 49
/// This class lets you perform tasks such as:
///
/// * Play an animation [forward] or in [reverse], or [stop] an animation.
/// * Set the animation to a specific [value].
/// * Define the [upperBound] and [lowerBound] values of an animation.
/// * Create a [fling] animation effect using a physics simulation.
///
50 51 52 53
/// By default, an [AnimationController] linearly produces values that range
/// from 0.0 to 1.0, during a given duration. The animation controller generates
/// a new value whenever the device running your app is ready to display a new
/// frame (typically, this rate is around 60 values per second).
54
///
55 56 57 58 59 60 61 62 63
/// An AnimationController needs a [TickerProvider], which is configured using
/// the `vsync` argument on the constructor. 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.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
///
/// The methods that start animations return a [TickerFuture] object which
/// completes when the animation completes successfully, and never throws an
/// error; if the animation is canceled, the future never completes. This object
/// also has a [TickerFuture.orCancel] property which returns a future that
/// completes when the animation completes successfully, and completes with an
/// error when the animation is aborted.
///
/// This can be used to write code such as:
///
/// ```dart
/// Future<Null> fadeOutAndUpdateState() async {
///   try {
///     await fadeAnimationController.forward().orCancel;
///     await sizeAnimationController.forward().orCancel;
///     setState(() {
///       dismissed = true;
///     });
///   } on TickerCanceled {
///     // the animation got canceled, probably because we were disposed
///   }
/// }
/// ```
///
88
/// ...which asynchronously runs one animation, then runs another, then changes
89 90 91 92
/// the state of the widget, without having to verify [State.mounted] is still
/// true at each step, and without having to chain futures together explicitly.
/// (This assumes that the controllers are created in [State.initState] and
/// disposed in [State.dispose].)
93
class AnimationController extends Animation<double>
94
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
95 96 97

  /// Creates an animation controller.
  ///
98 99 100
  /// * [value] is the initial value of the animation. If defaults to the lower
  ///   bound.
  ///
101
  /// * [duration] is the length of time this animation should last.
102 103 104 105 106 107 108 109 110 111 112 113 114
  ///
  /// * [debugLabel] is a string to help identify this animation during
  ///   debugging (used by [toString]).
  ///
  /// * [lowerBound] is the smallest value this animation can obtain and the
  ///   value at which this animation is deemed to be dismissed. It cannot be
  ///   null.
  ///
  /// * [upperBound] is the largest value this animation can obtain and the
  ///   value at which this animation is deemed to be completed. It cannot be
  ///   null.
  ///
  /// * `vsync` is the [TickerProvider] for the current context. It can be
115
  ///   changed by calling [resync]. It is required and must not be null. See
116
  ///   [TickerProvider] for advice on obtaining a ticker provider.
117 118 119 120
  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
121 122
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
123
    @required TickerProvider vsync,
124 125 126 127 128
  }) : assert(lowerBound != null),
       assert(upperBound != null),
       assert(upperBound >= lowerBound),
       assert(vsync != null),
       _direction = _AnimationDirection.forward {
129
    _ticker = vsync.createTicker(_tick);
130
    _internalSetValue(value ?? lowerBound);
131 132
  }

133 134
  /// Creates an animation controller with no upper or lower bound for its value.
  ///
135
  /// * [value] is the initial value of the animation.
136
  ///
137
  /// * [duration] is the length of time this animation should last.
138 139 140 141 142
  ///
  /// * [debugLabel] is a string to help identify this animation during
  ///   debugging (used by [toString]).
  ///
  /// * `vsync` is the [TickerProvider] for the current context. It can be
143
  ///   changed by calling [resync]. It is required and must not be null. See
144
  ///   [TickerProvider] for advice on obtaining a ticker provider.
145 146
  ///
  /// This constructor is most useful for animations that will be driven using a
147
  /// physics simulation, especially when the physics simulation has no
148
  /// pre-determined bounds.
149
  AnimationController.unbounded({
150
    double value = 0.0,
151
    this.duration,
152 153
    this.debugLabel,
    @required TickerProvider vsync,
154 155
  }) : assert(value != null),
       assert(vsync != null),
156 157
       lowerBound = double.negativeInfinity,
       upperBound = double.infinity,
158
       _direction = _AnimationDirection.forward {
159
    _ticker = vsync.createTicker(_tick);
160
    _internalSetValue(value);
161 162 163 164 165 166 167 168
  }

  /// The value at which this animation is deemed to be dismissed.
  final double lowerBound;

  /// The value at which this animation is deemed to be completed.
  final double upperBound;

169 170 171 172
  /// A label that is used in the [toString] output. Intended to aid with
  /// identifying animation controller instances in debug output.
  final String debugLabel;

173 174 175
  /// Returns an [Animation<double>] for this animation controller, so that a
  /// pointer to this object can be passed around without allowing users of that
  /// pointer to mutate the [AnimationController] state.
176 177 178 179 180
  Animation<double> get view => this;

  /// The length of time this animation should last.
  Duration duration;

181
  Ticker _ticker;
182 183 184

  /// Recreates the [Ticker] with the new [TickerProvider].
  void resync(TickerProvider vsync) {
185
    final Ticker oldTicker = _ticker;
186 187 188 189
    _ticker = vsync.createTicker(_tick);
    _ticker.absorbTicker(oldTicker);
  }

190 191
  Simulation _simulation;

192
  /// The current value of the animation.
193
  ///
194 195 196 197 198 199
  /// Setting this value notifies all the listeners that the value
  /// changed.
  ///
  /// Setting this value also stops the controller if it is currently
  /// running; if this happens, it also notifies all the status
  /// listeners.
200
  @override
201
  double get value => _value;
202
  double _value;
203 204 205 206 207 208 209
  /// Stops the animation controller and sets the current value of the
  /// animation.
  ///
  /// The new value is clamped to the range set by [lowerBound] and [upperBound].
  ///
  /// Value listeners are notified even if this does not change the value.
  /// Status listeners are notified if the animation was previously playing.
210 211 212 213
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
214 215 216 217 218 219 220 221 222
  ///
  /// See also:
  ///
  ///  * [reset], which is equivalent to setting [value] to [lowerBound].
  ///  * [stop], which aborts the animation without changing its value or status
  ///    and without dispatching any notifications other than completing or
  ///    canceling the [TickerFuture].
  ///  * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat],
  ///    which start the animation controller.
223
  set value(double newValue) {
224
    assert(newValue != null);
225
    stop();
226
    _internalSetValue(newValue);
227
    notifyListeners();
228 229
    _checkStatusChanged();
  }
230

231 232
  /// Sets the controller's value to [lowerBound], stopping the animation (if
  /// in progress), and resetting to its beginning point, or dismissed state.
233 234 235 236 237 238 239 240 241 242 243 244
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
  ///
  /// See also:
  ///
  ///  * [value], which can be explicitly set to a specific value as desired.
  ///  * [forward], which starts the animation in the forward direction.
  ///  * [stop], which aborts the animation without changing its value or status
  ///    and without dispatching any notifications other than completing or
  ///    canceling the [TickerFuture].
245 246 247
  void reset() {
    value = lowerBound;
  }
248

249 250 251 252
  /// The rate of change of [value] per second.
  ///
  /// If [isAnimating] is false, then [value] is not changing and the rate of
  /// change is zero.
253 254 255
  double get velocity {
    if (!isAnimating)
      return 0.0;
256
    return _simulation.dx(lastElapsedDuration.inMicroseconds.toDouble() / Duration.microsecondsPerSecond);
257 258
  }

259 260 261 262 263 264
  void _internalSetValue(double newValue) {
    _value = newValue.clamp(lowerBound, upperBound);
    if (_value == lowerBound) {
      _status = AnimationStatus.dismissed;
    } else if (_value == upperBound) {
      _status = AnimationStatus.completed;
265
    } else {
266 267 268
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
269
    }
270 271
  }

272 273
  /// The amount of time that has passed between the time the animation started
  /// and the most recent tick of the animation.
274
  ///
275
  /// If the controller is not animating, the last elapsed duration is null.
276 277 278
  Duration get lastElapsedDuration => _lastElapsedDuration;
  Duration _lastElapsedDuration;

279
  /// Whether this animation is currently animating in either the forward or reverse direction.
280 281 282 283 284
  ///
  /// This is separate from whether it is actively ticking. An animation
  /// controller's ticker might get muted, in which case the animation
  /// controller's callbacks will no longer fire even though time is continuing
  /// to pass. See [Ticker.muted] and [TickerMode].
285
  bool get isAnimating => _ticker != null && _ticker.isActive;
286

Adam Barth's avatar
Adam Barth committed
287 288
  _AnimationDirection _direction;

289
  @override
290 291
  AnimationStatus get status => _status;
  AnimationStatus _status;
292 293

  /// Starts running this animation forwards (towards the end).
294
  ///
295 296 297 298 299
  /// Returns a [TickerFuture] that completes when the animation is complete.
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
300 301 302 303
  ///
  /// During the animation, [status] is reported as [AnimationStatus.forward],
  /// which switches to [AnimationStatus.completed] when [upperBound] is
  /// reached at the end of the animation.
304
  TickerFuture forward({ double from }) {
305 306 307 308 309 310 311 312 313
    assert(() {
      if (duration == null) {
        throw new FlutterError(
          'AnimationController.forward() called with no default Duration.\n'
          'The "duration" property should be set, either in the constructor or later, before '
          'calling the forward() function.'
        );
      }
      return true;
314
    }());
315
    _direction = _AnimationDirection.forward;
316 317
    if (from != null)
      value = from;
318
    return _animateToInternal(upperBound);
319 320
  }

Adam Barth's avatar
Adam Barth committed
321
  /// Starts running this animation in reverse (towards the beginning).
322
  ///
323 324 325 326 327
  /// Returns a [TickerFuture] that completes when the animation is dismissed.
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
328 329 330 331
  ///
  /// During the animation, [status] is reported as [AnimationStatus.reverse],
  /// which switches to [AnimationStatus.dismissed] when [lowerBound] is
  /// reached at the end of the animation.
332
  TickerFuture reverse({ double from }) {
333 334 335 336 337 338 339 340 341
    assert(() {
      if (duration == null) {
        throw new FlutterError(
          'AnimationController.reverse() called with no default Duration.\n'
          'The "duration" property should be set, either in the constructor or later, before '
          'calling the reverse() function.'
        );
      }
      return true;
342
    }());
343
    _direction = _AnimationDirection.reverse;
344 345
    if (from != null)
      value = from;
346
    return _animateToInternal(lowerBound);
347 348
  }

349
  /// Drives the animation from its current value to target.
350
  ///
351 352 353 354 355
  /// Returns a [TickerFuture] that completes when the animation is complete.
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
356 357 358 359 360
  ///
  /// During the animation, [status] is reported as [AnimationStatus.forward]
  /// regardless of whether `target` > [value] or not. At the end of the
  /// animation, when `target` is reached, [status] is reported as
  /// [AnimationStatus.completed].
361
  TickerFuture animateTo(double target, { Duration duration, Curve curve = Curves.linear }) {
362 363 364 365
    _direction = _AnimationDirection.forward;
    return _animateToInternal(target, duration: duration, curve: curve);
  }

366
  TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) {
367 368
    Duration simulationDuration = duration;
    if (simulationDuration == null) {
369 370 371 372 373 374 375 376 377 378
      assert(() {
        if (this.duration == null) {
          throw new FlutterError(
            'AnimationController.animateTo() called with no explicit Duration and no default Duration.\n'
            'Either the "duration" argument to the animateTo() method should be provided, or the '
            '"duration" property should be set, either in the constructor or later, before '
            'calling the animateTo() function.'
          );
        }
        return true;
379
      }());
380 381
      final double range = upperBound - lowerBound;
      final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
382
      simulationDuration = this.duration * remainingFraction;
383 384
    } else if (target == value) {
      // Already at target, don't animate.
385
      simulationDuration = Duration.zero;
386
    }
387
    stop();
388
    if (simulationDuration == Duration.zero) {
389 390 391 392
      if (value != target) {
        _value = target.clamp(lowerBound, upperBound);
        notifyListeners();
      }
393 394 395
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
396
      _checkStatusChanged();
397
      return new TickerFuture.complete();
398
    }
399
    assert(simulationDuration > Duration.zero);
400
    assert(!isAnimating);
401
    return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));
402 403
  }

404 405
  /// Starts running this animation in the forward direction, and
  /// restarts the animation when it completes.
406 407
  ///
  /// Defaults to repeating between the lower and upper bounds.
408
  ///
409
  /// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future
410 411 412 413 414 415
  /// completes with an error when the animation is stopped (e.g. with [stop]).
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
  TickerFuture repeat({ double min, double max, Duration period }) {
416 417
    min ??= lowerBound;
    max ??= upperBound;
418
    period ??= duration;
419
    assert(() {
420
      if (period == null) {
421
        throw new FlutterError(
422 423
          'AnimationController.repeat() called without an explicit period and with no default Duration.\n'
          'Either the "period" argument to the repeat() method should be provided, or the '
424 425 426 427 428
          '"duration" property should be set, either in the constructor or later, before '
          'calling the repeat() function.'
        );
      }
      return true;
429
    }());
430 431 432
    return animateWith(new _RepeatingSimulation(min, max, period));
  }

433 434 435 436 437 438 439
  /// Drives the animation with a critically damped spring (within [lowerBound]
  /// and [upperBound]) and initial velocity.
  ///
  /// If velocity is positive, the animation will complete, otherwise it will
  /// dismiss.
  ///
  /// Returns a [TickerFuture] that completes when the animation is complete.
440
  ///
441 442 443
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
444
  TickerFuture fling({ double velocity = 1.0 }) {
Adam Barth's avatar
Adam Barth committed
445
    _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
446 447
    final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance
                                         : upperBound + _kFlingTolerance.distance;
448
    final Simulation simulation = new SpringSimulation(_kFlingSpringDescription, value, target, velocity)
449 450
      ..tolerance = _kFlingTolerance;
    return animateWith(simulation);
451 452 453
  }

  /// Drives the animation according to the given simulation.
454 455 456 457 458 459 460
  ///
  /// Returns a [TickerFuture] that completes when the animation is complete.
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
  TickerFuture animateWith(Simulation simulation) {
461 462 463 464
    stop();
    return _startSimulation(simulation);
  }

465
  TickerFuture _startSimulation(Simulation simulation) {
466 467 468
    assert(simulation != null);
    assert(!isAnimating);
    _simulation = simulation;
469
    _lastElapsedDuration = Duration.zero;
470
    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
471
    final Future<Null> result = _ticker.start();
472 473 474
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
475 476
    _checkStatusChanged();
    return result;
477 478
  }

479
  /// Stops running this animation.
480 481 482
  ///
  /// This does not trigger any notifications. The animation stops in its
  /// current state.
483 484 485 486
  ///
  /// By default, the most recently returned [TickerFuture] is marked as having
  /// been canceled, meaning the future never completes and its
  /// [TickerFuture.orCancel] derivative future completes with a [TickerCanceled]
487
  /// error. By passing the `canceled` argument with the value false, this is
488
  /// reversed, and the futures complete successfully.
489 490 491 492 493 494 495
  ///
  /// See also:
  ///
  ///  * [reset], which stops the animation and resets it to the [lowerBound],
  ///    and which does send notifications.
  ///  * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat],
  ///    which restart the animation controller.
496
  void stop({ bool canceled = true }) {
497
    _simulation = null;
498
    _lastElapsedDuration = null;
499
    _ticker.stop(canceled: canceled);
500 501
  }

502 503
  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
504 505 506 507
  ///
  /// The most recently returned [TickerFuture], if any, is marked as having been
  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
  /// derivative future completes with a [TickerCanceled] error.
508
  @override
509
  void dispose() {
510 511 512 513
    assert(() {
      if (_ticker == null) {
        throw new FlutterError(
          'AnimationController.dispose() called more than once.\n'
514 515 516
          'A given $runtimeType cannot be disposed more than once.\n'
          'The following $runtimeType object was disposed multiple times:\n'
          '  $this'
517 518 519
        );
      }
      return true;
520
    }());
521
    _ticker.dispose();
522
    _ticker = null;
523
    super.dispose();
524 525
  }

526
  AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
527
  void _checkStatusChanged() {
528
    final AnimationStatus newStatus = status;
529 530
    if (_lastReportedStatus != newStatus) {
      _lastReportedStatus = newStatus;
531
      notifyStatusListeners(newStatus);
532
    }
533 534
  }

535
  void _tick(Duration elapsed) {
536
    _lastElapsedDuration = elapsed;
537
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
538
    assert(elapsedInSeconds >= 0.0);
539
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
540 541 542 543
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
544
      stop(canceled: false);
545
    }
546 547 548 549
    notifyListeners();
    _checkStatusChanged();
  }

550
  @override
551
  String toStringDetails() {
552 553 554 555
    final String paused = isAnimating ? '' : '; paused';
    final String ticker = _ticker == null ? '; DISPOSED' : (_ticker.muted ? '; silenced' : '');
    final String label = debugLabel == null ? '' : '; for $debugLabel';
    final String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
556
    return '$more$paused$ticker$label';
557 558 559
  }
}

560 561
class _InterpolationSimulation extends Simulation {
  _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve)
562 563 564
    : assert(_begin != null),
      assert(_end != null),
      assert(duration != null && duration.inMicroseconds > 0),
565
      _durationInSeconds = duration.inMicroseconds / Duration.microsecondsPerSecond;
566 567 568 569 570 571

  final double _durationInSeconds;
  final double _begin;
  final double _end;
  final Curve _curve;

572
  @override
573
  double x(double timeInSeconds) {
574
    final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
575 576 577 578 579 580 581 582
    if (t == 0.0)
      return _begin;
    else if (t == 1.0)
      return _end;
    else
      return _begin + (_end - _begin) * _curve.transform(t);
  }

583
  @override
584
  double dx(double timeInSeconds) {
585
    final double epsilon = tolerance.time;
586 587
    return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
  }
588

589
  @override
590 591 592
  bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
}

593 594
class _RepeatingSimulation extends Simulation {
  _RepeatingSimulation(this.min, this.max, Duration period)
595
    : _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond {
596 597 598 599 600 601 602 603
    assert(_periodInSeconds > 0.0);
  }

  final double min;
  final double max;

  final double _periodInSeconds;

604
  @override
605 606 607
  double x(double timeInSeconds) {
    assert(timeInSeconds >= 0.0);
    final double t = (timeInSeconds / _periodInSeconds) % 1.0;
608
    return ui.lerpDouble(min, max, t);
609 610
  }

611
  @override
612
  double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
613

614
  @override
615 616
  bool isDone(double timeInSeconds) => false;
}