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

5

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
import 'package:flutter/semantics.dart';
12 13

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

17 18 19 20 21
export 'package:flutter/physics.dart' show Simulation, SpringDescription;
export 'package:flutter/scheduler.dart' show TickerFuture, TickerProvider;

export 'animation.dart' show Animation, AnimationStatus;
export 'curves.dart' show Curve;
22

23 24
const String _flutterAnimationLibrary = 'package:flutter/animation.dart';

25
// Examples can assume:
26 27
// late AnimationController _controller, fadeAnimationController, sizeAnimationController;
// late bool dismissed;
28
// void setState(VoidCallback fn) { }
29

Adam Barth's avatar
Adam Barth committed
30 31 32 33 34 35
/// 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.
36
  reverse,
Adam Barth's avatar
Adam Barth committed
37 38
}

39
final SpringDescription _kFlingSpringDescription = SpringDescription.withDampingRatio(
40
  mass: 1.0,
41
  stiffness: 500.0,
42 43
);

44
const Tolerance _kFlingTolerance = Tolerance(
45
  velocity: double.infinity,
46 47 48
  distance: 0.01,
);

49 50
/// Configures how an [AnimationController] behaves when animations are
/// disabled.
51 52
///
/// When [AccessibilityFeatures.disableAnimations] is true, the device is asking
53
/// Flutter to reduce or disable animations as much as possible. To honor this,
54 55 56
/// we reduce the duration and the corresponding number of frames for
/// animations. This enum is used to allow certain [AnimationController]s to opt
/// out of this behavior.
57 58
///
/// For example, the [AnimationController] which controls the physics simulation
59
/// for a scrollable list will have [AnimationBehavior.preserve], so that when
60 61 62 63 64 65 66 67 68 69 70 71 72 73
/// a user attempts to scroll it does not jump to the end/beginning too quickly.
enum AnimationBehavior {
  /// The [AnimationController] will reduce its duration when
  /// [AccessibilityFeatures.disableAnimations] is true.
  normal,

  /// The [AnimationController] will preserve its behavior.
  ///
  /// This is the default for repeating animations in order to prevent them from
  /// flashing rapidly on the screen if the widget does not take the
  /// [AccessibilityFeatures.disableAnimations] flag into account.
  preserve,
}

74 75
/// A controller for an animation.
///
76 77 78 79 80 81 82
/// 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.
///
83 84 85 86
/// 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).
87
///
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
/// ## Ticker providers
///
/// An [AnimationController] needs a [TickerProvider], which is configured using
/// the `vsync` argument on the constructor.
///
/// The [TickerProvider] interface describes a factory for [Ticker] objects. A
/// [Ticker] is an object that knows how to register itself with the
/// [SchedulerBinding] and fires a callback every frame. The
/// [AnimationController] class uses a [Ticker] to step through the animation
/// that it controls.
///
/// If an [AnimationController] is being created from a [State], then the State
/// can use the [TickerProviderStateMixin] and [SingleTickerProviderStateMixin]
/// classes to implement the [TickerProvider] interface. The
/// [TickerProviderStateMixin] class always works for this purpose; the
/// [SingleTickerProviderStateMixin] is slightly more efficient in the case of
/// the class only ever needing one [Ticker] (e.g. if the class creates only a
/// single [AnimationController] during its entire lifetime).
///
/// 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.
///
/// ## Life cycle
///
/// An [AnimationController] should be [dispose]d when it is no longer needed.
/// This reduces the likelihood of leaks. When used with a [StatefulWidget], it
/// is common for an [AnimationController] to be created in the
/// [State.initState] method and then disposed in the [State.dispose] method.
///
/// ## Using [Future]s with [AnimationController]
121 122 123 124 125 126 127 128
///
/// 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.
///
129 130 131
/// This can be used to write code such as the `fadeOutAndUpdateState` method
/// below.
///
132
/// {@tool snippet}
133
///
Dan Field's avatar
Dan Field committed
134
/// Here is a stateful `Foo` widget. Its [State] uses the
135
/// [SingleTickerProviderStateMixin] to implement the necessary
Dan Field's avatar
Dan Field committed
136 137 138 139 140
/// [TickerProvider], creating its controller in the [State.initState] method
/// and disposing of it in the [State.dispose] method. The duration of the
/// controller is configured from a property in the `Foo` widget; as that
/// changes, the [State.didUpdateWidget] method is used to update the
/// controller.
141 142 143
///
/// ```dart
/// class Foo extends StatefulWidget {
144
///   const Foo({ super.key, required this.duration });
145 146 147 148
///
///   final Duration duration;
///
///   @override
149
///   State<Foo> createState() => _FooState();
150 151 152
/// }
///
/// class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
153
///   late AnimationController _controller;
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
///
///   @override
///   void initState() {
///     super.initState();
///     _controller = AnimationController(
///       vsync: this, // the SingleTickerProviderStateMixin
///       duration: widget.duration,
///     );
///   }
///
///   @override
///   void didUpdateWidget(Foo oldWidget) {
///     super.didUpdateWidget(oldWidget);
///     _controller.duration = widget.duration;
///   }
///
///   @override
///   void dispose() {
///     _controller.dispose();
///     super.dispose();
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return Container(); // ...
///   }
/// }
/// ```
182
/// {@end-tool}
183
/// {@tool snippet}
184 185 186
///
/// The following method (for a [State] subclass) drives two animation
/// controllers using Dart's asynchronous syntax for awaiting [Future] objects:
187 188
///
/// ```dart
189
/// Future<void> fadeOutAndUpdateState() async {
190 191 192 193 194 195 196 197 198 199 200
///   try {
///     await fadeAnimationController.forward().orCancel;
///     await sizeAnimationController.forward().orCancel;
///     setState(() {
///       dismissed = true;
///     });
///   } on TickerCanceled {
///     // the animation got canceled, probably because we were disposed
///   }
/// }
/// ```
201
/// {@end-tool}
202
///
203 204 205 206 207 208 209 210
/// The assumption in the code above is that the animation controllers are being
/// disposed in the [State] subclass' override of the [State.dispose] method.
/// Since disposing the controller cancels the animation (raising a
/// [TickerCanceled] exception), the code here can skip verifying whether
/// [State.mounted] is still true at each step. (Again, this assumes that the
/// controllers are created in [State.initState] and disposed in
/// [State.dispose], as described in the previous section.)
///
211 212 213 214 215 216 217 218 219 220 221 222 223
/// {@tool dartpad}
/// This example shows how to use [AnimationController] and
/// [SlideTransition] to create an animated digit like you might find
/// on an old pinball machine our your car's odometer.  New digit
/// values slide into place from below, as the old value slides
/// upwards and out of view. Taps that occur while the controller is
/// already animating cause the controller's
/// [AnimationController.duration] to be reduced so that the visuals
/// don't fall behind.
///
/// ** See code in examples/api/lib/animation/animation_controller/animated_digit.0.dart **
/// {@end-tool}

224 225 226 227
/// See also:
///
///  * [Tween], the base class for converting an [AnimationController] to a
///    range of values of other types.
228
class AnimationController extends Animation<double>
229
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
230 231
  /// Creates an animation controller.
  ///
232
  /// * `value` is the initial value of the animation. If defaults to the lower
233 234
  ///   bound.
  ///
235
  /// * [duration] is the length of time this animation should last.
236 237 238 239 240 241 242 243 244 245 246 247
  ///
  /// * [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.
  ///
248 249 250
  /// * `vsync` is the required [TickerProvider] for the current context. It can
  ///   be changed by calling [resync]. See [TickerProvider] for advice on
  ///   obtaining a ticker provider.
251
  AnimationController({
252
    double? value,
253
    this.duration,
254
    this.reverseDuration,
255
    this.debugLabel,
256 257
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
258
    this.animationBehavior = AnimationBehavior.normal,
259
    required TickerProvider vsync,
260
  }) : assert(upperBound >= lowerBound),
261
       _direction = _AnimationDirection.forward {
262 263 264
    if (kFlutterMemoryAllocationsEnabled) {
      _maybeDispatchObjectCreation();
    }
265
    _ticker = vsync.createTicker(_tick);
266
    _internalSetValue(value ?? lowerBound);
267 268
  }

269 270
  /// Creates an animation controller with no upper or lower bound for its
  /// value.
271
  ///
272
  /// * [value] is the initial value of the animation.
273
  ///
274
  /// * [duration] is the length of time this animation should last.
275 276 277 278
  ///
  /// * [debugLabel] is a string to help identify this animation during
  ///   debugging (used by [toString]).
  ///
279 280 281
  /// * `vsync` is the required [TickerProvider] for the current context. It can
  ///   be changed by calling [resync]. See [TickerProvider] for advice on
  ///   obtaining a ticker provider.
282 283
  ///
  /// This constructor is most useful for animations that will be driven using a
284
  /// physics simulation, especially when the physics simulation has no
285
  /// pre-determined bounds.
286
  AnimationController.unbounded({
287
    double value = 0.0,
288
    this.duration,
289
    this.reverseDuration,
290
    this.debugLabel,
291
    required TickerProvider vsync,
292
    this.animationBehavior = AnimationBehavior.preserve,
293
  }) : lowerBound = double.negativeInfinity,
294
       upperBound = double.infinity,
295
       _direction = _AnimationDirection.forward {
296 297 298
    if (kFlutterMemoryAllocationsEnabled) {
      _maybeDispatchObjectCreation();
    }
299
    _ticker = vsync.createTicker(_tick);
300
    _internalSetValue(value);
301 302
  }

303
  /// Dispatches event of object creation to [FlutterMemoryAllocations.instance].
304 305
  void _maybeDispatchObjectCreation() {
    if (kFlutterMemoryAllocationsEnabled) {
306
      FlutterMemoryAllocations.instance.dispatchObjectCreated(
307 308 309 310 311 312 313
        library: _flutterAnimationLibrary,
        className: '$AnimationController',
        object: this,
      );
    }
  }

314 315 316 317 318 319
  /// 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;

320 321
  /// A label that is used in the [toString] output. Intended to aid with
  /// identifying animation controller instances in debug output.
322
  final String? debugLabel;
323

324 325 326
  /// The behavior of the controller when [AccessibilityFeatures.disableAnimations]
  /// is true.
  ///
327
  /// Defaults to [AnimationBehavior.normal] for the [AnimationController.new]
328
  /// constructor, and [AnimationBehavior.preserve] for the
329
  /// [AnimationController.unbounded] constructor.
330 331
  final AnimationBehavior animationBehavior;

332 333 334
  /// 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.
335 336 337
  Animation<double> get view => this;

  /// The length of time this animation should last.
338 339 340
  ///
  /// If [reverseDuration] is specified, then [duration] is only used when going
  /// [forward]. Otherwise, it specifies the duration going in both directions.
341
  Duration? duration;
342

343 344
  /// The length of time this animation should last when going in [reverse].
  ///
345
  /// The value of [duration] is used if [reverseDuration] is not specified or
346
  /// set to null.
347
  Duration? reverseDuration;
348

349
  Ticker? _ticker;
350 351 352

  /// Recreates the [Ticker] with the new [TickerProvider].
  void resync(TickerProvider vsync) {
353
    final Ticker oldTicker = _ticker!;
354
    _ticker = vsync.createTicker(_tick);
355
    _ticker!.absorbTicker(oldTicker);
356 357
  }

358
  Simulation? _simulation;
359

360
  /// The current value of the animation.
361
  ///
362 363 364 365 366 367
  /// 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.
368
  @override
369
  double get value => _value;
370
  late double _value;
371 372 373
  /// Stops the animation controller and sets the current value of the
  /// animation.
  ///
374 375
  /// The new value is clamped to the range set by [lowerBound] and
  /// [upperBound].
376 377 378
  ///
  /// Value listeners are notified even if this does not change the value.
  /// Status listeners are notified if the animation was previously playing.
379 380 381 382
  ///
  /// 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.
383 384 385 386 387 388 389 390 391
  ///
  /// 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.
392
  set value(double newValue) {
393
    stop();
394
    _internalSetValue(newValue);
395
    notifyListeners();
396 397
    _checkStatusChanged();
  }
398

399 400
  /// Sets the controller's value to [lowerBound], stopping the animation (if
  /// in progress), and resetting to its beginning point, or dismissed state.
401 402 403 404 405 406 407 408 409 410 411 412
  ///
  /// 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].
413 414 415
  void reset() {
    value = lowerBound;
  }
416

417 418 419 420
  /// The rate of change of [value] per second.
  ///
  /// If [isAnimating] is false, then [value] is not changing and the rate of
  /// change is zero.
421
  double get velocity {
422
    if (!isAnimating) {
423
      return 0.0;
424
    }
425
    return _simulation!.dx(lastElapsedDuration!.inMicroseconds.toDouble() / Duration.microsecondsPerSecond);
426 427
  }

428
  void _internalSetValue(double newValue) {
429
    _value = clampDouble(newValue, lowerBound, upperBound);
430 431 432 433
    if (_value == lowerBound) {
      _status = AnimationStatus.dismissed;
    } else if (_value == upperBound) {
      _status = AnimationStatus.completed;
434
    } else {
435 436 437
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
438
    }
439 440
  }

441 442
  /// The amount of time that has passed between the time the animation started
  /// and the most recent tick of the animation.
443
  ///
444
  /// If the controller is not animating, the last elapsed duration is null.
445 446
  Duration? get lastElapsedDuration => _lastElapsedDuration;
  Duration? _lastElapsedDuration;
447

448
  /// Whether this animation is currently animating in either the forward or reverse direction.
449 450 451 452 453
  ///
  /// 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].
454
  bool get isAnimating => _ticker != null && _ticker!.isActive;
455

Adam Barth's avatar
Adam Barth committed
456 457
  _AnimationDirection _direction;

458
  @override
459
  AnimationStatus get status => _status;
460
  late AnimationStatus _status;
461 462

  /// Starts running this animation forwards (towards the end).
463
  ///
464 465 466 467 468
  /// 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.
469 470 471 472
  ///
  /// During the animation, [status] is reported as [AnimationStatus.forward],
  /// which switches to [AnimationStatus.completed] when [upperBound] is
  /// reached at the end of the animation.
473
  TickerFuture forward({ double? from }) {
474 475
    assert(() {
      if (duration == null) {
476
        throw FlutterError(
477
          'AnimationController.forward() called with no default duration.\n'
478
          'The "duration" property should be set, either in the constructor or later, before '
479
          'calling the forward() function.',
480 481 482
        );
      }
      return true;
483
    }());
484 485 486
    assert(
      _ticker != null,
      'AnimationController.forward() called after AnimationController.dispose()\n'
487
      'AnimationController methods should not be used after calling dispose.',
488
    );
489
    _direction = _AnimationDirection.forward;
490
    if (from != null) {
491
      value = from;
492
    }
493
    return _animateToInternal(upperBound);
494 495
  }

Adam Barth's avatar
Adam Barth committed
496
  /// Starts running this animation in reverse (towards the beginning).
497
  ///
498 499 500 501 502
  /// 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.
503 504 505 506
  ///
  /// During the animation, [status] is reported as [AnimationStatus.reverse],
  /// which switches to [AnimationStatus.dismissed] when [lowerBound] is
  /// reached at the end of the animation.
507
  TickerFuture reverse({ double? from }) {
508
    assert(() {
509
      if (duration == null && reverseDuration == null) {
510
        throw FlutterError(
511 512
          'AnimationController.reverse() called with no default duration or reverseDuration.\n'
          'The "duration" or "reverseDuration" property should be set, either in the constructor or later, before '
513
          'calling the reverse() function.',
514 515 516
        );
      }
      return true;
517
    }());
518 519 520
    assert(
      _ticker != null,
      'AnimationController.reverse() called after AnimationController.dispose()\n'
521
      'AnimationController methods should not be used after calling dispose.',
522
    );
523
    _direction = _AnimationDirection.reverse;
524
    if (from != null) {
525
      value = from;
526
    }
527
    return _animateToInternal(lowerBound);
528 529
  }

530
  /// Drives the animation from its current value to target.
531
  ///
532 533 534 535 536
  /// 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.
537 538 539 540 541
  ///
  /// 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].
542 543 544 545
  ///
  /// If the `target` argument is the same as the current [value] of the
  /// animation, then this won't animate, and the returned [TickerFuture] will
  /// be already complete.
546
  TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear }) {
547 548 549 550 551 552
    assert(() {
      if (this.duration == null && duration == null) {
        throw 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 '
553
          'calling the animateTo() function.',
554 555 556 557
        );
      }
      return true;
    }());
558 559 560
    assert(
      _ticker != null,
      'AnimationController.animateTo() called after AnimationController.dispose()\n'
561
      'AnimationController methods should not be used after calling dispose.',
562
    );
563 564 565 566
    _direction = _AnimationDirection.forward;
    return _animateToInternal(target, duration: duration, curve: curve);
  }

567 568 569 570 571 572 573 574 575 576 577 578
  /// Drives the animation from its current value to target.
  ///
  /// 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.
  ///
  /// During the animation, [status] is reported as [AnimationStatus.reverse]
  /// regardless of whether `target` < [value] or not. At the end of the
  /// animation, when `target` is reached, [status] is reported as
  /// [AnimationStatus.dismissed].
579
  TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear }) {
580 581 582 583 584 585
    assert(() {
      if (this.duration == null && reverseDuration == null && duration == null) {
        throw FlutterError(
          'AnimationController.animateBack() called with no explicit duration and no default duration or reverseDuration.\n'
          'Either the "duration" argument to the animateBack() method should be provided, or the '
          '"duration" or "reverseDuration" property should be set, either in the constructor or later, before '
586
          'calling the animateBack() function.',
587 588 589 590
        );
      }
      return true;
    }());
591 592 593
    assert(
      _ticker != null,
      'AnimationController.animateBack() called after AnimationController.dispose()\n'
594
      'AnimationController methods should not be used after calling dispose.',
595
    );
596 597 598 599
    _direction = _AnimationDirection.reverse;
    return _animateToInternal(target, duration: duration, curve: curve);
  }

600
  TickerFuture _animateToInternal(double target, { Duration? duration, Curve curve = Curves.linear }) {
601
    double scale = 1.0;
602
    if (SemanticsBinding.instance.disableAnimations) {
603
      switch (animationBehavior) {
604
        case AnimationBehavior.normal:
605 606
          // Since the framework cannot handle zero duration animations, we run it at 5% of the normal
          // duration to limit most animations to a single frame.
607 608 609
          // Ideally, the framework would be able to handle zero duration animations, however, the common
          // pattern of an eternally repeating animation might cause an endless loop if it weren't delayed
          // for at least one frame.
610 611 612 613 614
          scale = 0.05;
        case AnimationBehavior.preserve:
          break;
      }
    }
615
    Duration? simulationDuration = duration;
616
    if (simulationDuration == null) {
617 618
      assert(!(this.duration == null && _direction == _AnimationDirection.forward));
      assert(!(this.duration == null && _direction == _AnimationDirection.reverse && reverseDuration == null));
619 620
      final double range = upperBound - lowerBound;
      final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
621 622
      final Duration directionDuration =
        (_direction == _AnimationDirection.reverse && reverseDuration != null)
623 624
        ? reverseDuration!
        : this.duration!;
625
      simulationDuration = directionDuration * remainingFraction;
626 627
    } else if (target == value) {
      // Already at target, don't animate.
628
      simulationDuration = Duration.zero;
629
    }
630
    stop();
631
    if (simulationDuration == Duration.zero) {
632
      if (value != target) {
633
        _value = clampDouble(target, lowerBound, upperBound);
634 635
        notifyListeners();
      }
636 637 638
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
639
      _checkStatusChanged();
640
      return TickerFuture.complete();
641
    }
642
    assert(simulationDuration > Duration.zero);
643
    assert(!isAnimating);
644
    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
645 646
  }

647 648
  /// Starts running this animation in the forward direction, and
  /// restarts the animation when it completes.
649
  ///
650 651 652 653
  /// Defaults to repeating between the [lowerBound] and [upperBound] of the
  /// [AnimationController] when no explicit value is set for [min] and [max].
  ///
  /// With [reverse] set to true, instead of always starting over at [min]
654 655 656
  /// the starting value will alternate between [min] and [max] values on each
  /// repeat. The [status] will be reported as [AnimationStatus.reverse] when
  /// the animation runs from [max] to [min].
657
  ///
658 659 660 661
  /// Each run of the animation will have a duration of `period`. If `period` is not
  /// provided, [duration] will be used instead, which has to be set before [repeat] is
  /// called either in the constructor or later by using the [duration] setter.
  ///
662
  /// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future
663 664 665 666 667
  /// 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.
668
  TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {
669 670
    min ??= lowerBound;
    max ??= upperBound;
671
    period ??= duration;
672
    assert(() {
673
      if (period == null) {
674
        throw FlutterError(
675 676
          '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 '
677
          '"duration" property should be set, either in the constructor or later, before '
678
          'calling the repeat() function.',
679 680 681
        );
      }
      return true;
682
    }());
683 684
    assert(max >= min);
    assert(max <= upperBound && min >= lowerBound);
685
    stop();
686
    return _startSimulation(_RepeatingSimulation(_value, min, max, reverse, period!, _directionSetter));
687 688 689 690 691 692 693 694
  }

  void _directionSetter(_AnimationDirection direction) {
    _direction = direction;
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
    _checkStatusChanged();
695 696
  }

697 698
  /// Drives the animation with a spring (within [lowerBound] and [upperBound])
  /// and initial velocity.
699 700
  ///
  /// If velocity is positive, the animation will complete, otherwise it will
701 702 703
  /// dismiss. The velocity is specified in units per second. If the
  /// [SemanticsBinding.disableAnimations] flag is set, the velocity is somewhat
  /// arbitrarily multiplied by 200.
704
  ///
705 706 707 708 709
  /// The [springDescription] parameter can be used to specify a custom
  /// [SpringType.criticallyDamped] or [SpringType.overDamped] spring with which
  /// to drive the animation. By default, a [SpringType.criticallyDamped] spring
  /// is used. See [SpringDescription.withDampingRatio] for how to create a
  /// suitable [SpringDescription].
710
  ///
711 712
  /// The resulting spring simulation cannot be of type [SpringType.underDamped];
  /// such a spring would oscillate rather than fling.
713
  ///
714
  /// Returns a [TickerFuture] that completes when the animation is complete.
715
  ///
716 717 718
  /// 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.
719 720
  TickerFuture fling({ double velocity = 1.0, SpringDescription? springDescription, AnimationBehavior? animationBehavior }) {
    springDescription ??= _kFlingSpringDescription;
Adam Barth's avatar
Adam Barth committed
721
    _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
722 723
    final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance
                                         : upperBound + _kFlingTolerance.distance;
724 725
    double scale = 1.0;
    final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
726
    if (SemanticsBinding.instance.disableAnimations) {
727 728
      switch (behavior) {
        case AnimationBehavior.normal:
729
          scale = 200.0; // This is arbitrary (it was chosen because it worked for the drawer widget).
730 731 732 733
        case AnimationBehavior.preserve:
          break;
      }
    }
734
    final SpringSimulation simulation = SpringSimulation(springDescription, value, target, velocity * scale)
735
      ..tolerance = _kFlingTolerance;
736 737
    assert(
      simulation.type != SpringType.underDamped,
738 739 740 741
      'The specified spring simulation is of type SpringType.underDamped.\n'
      'An underdamped spring results in oscillation rather than a fling. '
      'Consider specifying a different springDescription, or use animateWith() '
      'with an explicit SpringSimulation if an underdamped spring is intentional.',
742
    );
743 744
    stop();
    return _startSimulation(simulation);
745 746 747
  }

  /// Drives the animation according to the given simulation.
748
  ///
749 750
  /// The values from the simulation are clamped to the [lowerBound] and
  /// [upperBound]. To avoid this, consider creating the [AnimationController]
751
  /// using the [AnimationController.unbounded] constructor.
752
  ///
753 754 755 756 757
  /// 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.
758 759 760
  ///
  /// The [status] is always [AnimationStatus.forward] for the entire duration
  /// of the simulation.
761
  TickerFuture animateWith(Simulation simulation) {
762 763 764
    assert(
      _ticker != null,
      'AnimationController.animateWith() called after AnimationController.dispose()\n'
765
      'AnimationController methods should not be used after calling dispose.',
766
    );
767
    stop();
768
    _direction = _AnimationDirection.forward;
769 770 771
    return _startSimulation(simulation);
  }

772
  TickerFuture _startSimulation(Simulation simulation) {
773 774
    assert(!isAnimating);
    _simulation = simulation;
775
    _lastElapsedDuration = Duration.zero;
776
    _value = clampDouble(simulation.x(0.0), lowerBound, upperBound);
777
    final TickerFuture result = _ticker!.start();
778 779 780
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
781 782
    _checkStatusChanged();
    return result;
783 784
  }

785
  /// Stops running this animation.
786 787 788
  ///
  /// This does not trigger any notifications. The animation stops in its
  /// current state.
789 790 791 792
  ///
  /// 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]
793
  /// error. By passing the `canceled` argument with the value false, this is
794
  /// reversed, and the futures complete successfully.
795 796 797 798 799 800 801
  ///
  /// 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.
802
  void stop({ bool canceled = true }) {
803 804 805
    assert(
      _ticker != null,
      'AnimationController.stop() called after AnimationController.dispose()\n'
806
      'AnimationController methods should not be used after calling dispose.',
807
    );
808
    _simulation = null;
809
    _lastElapsedDuration = null;
810
    _ticker!.stop(canceled: canceled);
811 812
  }

813 814
  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
815 816 817 818
  ///
  /// 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.
819
  @override
820
  void dispose() {
821 822
    assert(() {
      if (_ticker == null) {
823 824 825 826 827 828 829 830 831
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('AnimationController.dispose() called more than once.'),
          ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
          DiagnosticsProperty<AnimationController>(
            'The following $runtimeType object was disposed multiple times',
            this,
            style: DiagnosticsTreeStyle.errorProperty,
          ),
        ]);
832 833
      }
      return true;
834
    }());
835
    if (kFlutterMemoryAllocationsEnabled) {
836
      FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
837
    }
838
    _ticker!.dispose();
839
    _ticker = null;
840 841
    clearStatusListeners();
    clearListeners();
842
    super.dispose();
843 844
  }

845
  AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
846
  void _checkStatusChanged() {
847
    final AnimationStatus newStatus = status;
848 849
    if (_lastReportedStatus != newStatus) {
      _lastReportedStatus = newStatus;
850
      notifyStatusListeners(newStatus);
851
    }
852 853
  }

854
  void _tick(Duration elapsed) {
855
    _lastElapsedDuration = elapsed;
856
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
857
    assert(elapsedInSeconds >= 0.0);
858
    _value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound);
859
    if (_simulation!.isDone(elapsedInSeconds)) {
860 861 862
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
863
      stop(canceled: false);
864
    }
865 866 867 868
    notifyListeners();
    _checkStatusChanged();
  }

869
  @override
870
  String toStringDetails() {
871
    final String paused = isAnimating ? '' : '; paused';
872
    final String ticker = _ticker == null ? '; DISPOSED' : (_ticker!.muted ? '; silenced' : '');
873 874 875 876 877 878 879
    String label = '';
    assert(() {
      if (debugLabel != null) {
        label = '; for $debugLabel';
      }
      return true;
    }());
880
    final String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
881
    return '$more$paused$ticker$label';
882 883 884
  }
}

885
class _InterpolationSimulation extends Simulation {
886
  _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale)
887
    : assert(duration.inMicroseconds > 0),
888
      _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
889 890 891 892 893 894

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

895
  @override
896
  double x(double timeInSeconds) {
897
    final double t = clampDouble(timeInSeconds / _durationInSeconds, 0.0, 1.0);
898
    if (t == 0.0) {
899
      return _begin;
900
    } else if (t == 1.0) {
901
      return _end;
902
    } else {
903
      return _begin + (_end - _begin) * _curve.transform(t);
904
    }
905 906
  }

907
  @override
908
  double dx(double timeInSeconds) {
909
    final double epsilon = tolerance.time;
910 911
    return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
  }
912

913
  @override
914 915 916
  bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
}

917 918
typedef _DirectionSetter = void Function(_AnimationDirection direction);

919
class _RepeatingSimulation extends Simulation {
920
  _RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period, this.directionSetter)
921 922
      : _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
        _initialT = (max == min) ? 0.0 : (initialValue / (max - min)) * (period.inMicroseconds / Duration.microsecondsPerSecond) {
923
    assert(_periodInSeconds > 0.0);
924
    assert(_initialT >= 0.0);
925 926 927 928
  }

  final double min;
  final double max;
929
  final bool reverse;
930
  final _DirectionSetter directionSetter;
931 932

  final double _periodInSeconds;
933
  final double _initialT;
934

935
  @override
936 937
  double x(double timeInSeconds) {
    assert(timeInSeconds >= 0.0);
938 939 940

    final double totalTimeInSeconds = timeInSeconds + _initialT;
    final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0;
941
    final bool isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds).isOdd;
942

943
    if (reverse && isPlayingReverse) {
944
      directionSetter(_AnimationDirection.reverse);
945
      return ui.lerpDouble(max, min, t)!;
946
    } else {
947
      directionSetter(_AnimationDirection.forward);
948
      return ui.lerpDouble(min, max, t)!;
949
    }
950 951
  }

952
  @override
953
  double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
954

955
  @override
956 957
  bool isDone(double timeInSeconds) => false;
}