1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/widgets.dart';
6/// @docImport 'package:flutter_test/flutter_test.dart';
7library;
8
9import 'dart:ui' as ui show lerpDouble;
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/physics.dart';
13import 'package:flutter/scheduler.dart';
14import 'package:flutter/semantics.dart';
15
16import 'animation.dart';
17import 'curves.dart';
18import 'listener_helpers.dart';
19
20export 'package:flutter/physics.dart' show Simulation, SpringDescription;
21export 'package:flutter/scheduler.dart' show TickerFuture, TickerProvider;
22
23export 'animation.dart' show Animation, AnimationStatus;
24export 'curves.dart' show Curve;
25
26// Examples can assume:
27// late AnimationController _controller, fadeAnimationController, sizeAnimationController;
28// late bool dismissed;
29// void setState(VoidCallback fn) { }
30
31/// The direction in which an animation is running.
32enum _AnimationDirection {
33 /// The animation is running from beginning to end.
34 forward,
35
36 /// The animation is running backwards, from end to beginning.
37 reverse,
38}
39
40final SpringDescription _kFlingSpringDescription = SpringDescription.withDampingRatio(
41 mass: 1.0,
42 stiffness: 500.0,
43);
44
45const Tolerance _kFlingTolerance = Tolerance(velocity: double.infinity, distance: 0.01);
46
47/// Configures how an [AnimationController] behaves when animations are
48/// disabled.
49///
50/// When [AccessibilityFeatures.disableAnimations] is true, the device is asking
51/// Flutter to reduce or disable animations as much as possible. To honor this,
52/// we reduce the duration and the corresponding number of frames for
53/// animations. This enum is used to allow certain [AnimationController]s to opt
54/// out of this behavior.
55///
56/// For example, the [AnimationController] which controls the physics simulation
57/// for a scrollable list will have [AnimationBehavior.preserve], so that when
58/// a user attempts to scroll it does not jump to the end/beginning too quickly.
59enum AnimationBehavior {
60 /// The [AnimationController] will reduce its duration when
61 /// [AccessibilityFeatures.disableAnimations] is true.
62 normal,
63
64 /// The [AnimationController] will preserve its behavior.
65 ///
66 /// This is the default for repeating animations in order to prevent them from
67 /// flashing rapidly on the screen if the widget does not take the
68 /// [AccessibilityFeatures.disableAnimations] flag into account.
69 preserve,
70}
71
72/// A controller for an animation.
73///
74/// This class lets you perform tasks such as:
75///
76/// * Play an animation [forward] or in [reverse], or [stop] an animation.
77/// * Set the animation to a specific [value].
78/// * Define the [upperBound] and [lowerBound] values of an animation.
79/// * Create a [fling] animation effect using a physics simulation.
80///
81/// By default, an [AnimationController] linearly produces values that range
82/// from 0.0 to 1.0, during a given duration.
83///
84/// When the animation is actively animating, the animation controller generates
85/// a new value each time the device running your app is ready to display a new
86/// frame (typically, this rate is around 60–120 values per second).
87/// If the animation controller is associated with a [State]
88/// through a [TickerProvider], then its updates will be silenced when that
89/// [State]'s subtree is disabled as defined by [TickerMode]; time will still
90/// elapse, and methods like [forward] and [stop] can still be called and
91/// will change the value, but the controller will not generate new values
92/// on its own.
93///
94/// ## Ticker providers
95///
96/// An [AnimationController] needs a [TickerProvider], which is configured using
97/// the `vsync` argument on the constructor.
98/// The constructor uses the [TickerProvider] to create a [Ticker], which
99/// the [AnimationController] uses to step through the animation it controls.
100///
101/// For advice on obtaining a ticker provider, see [TickerProvider].
102/// Typically the relevant [State] serves as the ticker provider,
103/// after applying a suitable mixin (like [SingleTickerProviderStateMixin])
104/// to cause the [State] subclass to implement [TickerProvider].
105///
106/// ## Life cycle
107///
108/// An [AnimationController] should be [dispose]d when it is no longer needed.
109/// This reduces the likelihood of leaks. When used with a [StatefulWidget], it
110/// is common for an [AnimationController] to be created in the
111/// [State.initState] method and then disposed in the [State.dispose] method.
112///
113/// ## Using [Future]s with [AnimationController]
114///
115/// The methods that start animations return a [TickerFuture] object which
116/// completes when the animation completes successfully, and never throws an
117/// error; if the animation is canceled, the future never completes. This object
118/// also has a [TickerFuture.orCancel] property which returns a future that
119/// completes when the animation completes successfully, and completes with an
120/// error when the animation is aborted.
121///
122/// This can be used to write code such as the `fadeOutAndUpdateState` method
123/// below.
124///
125/// {@tool snippet}
126///
127/// Here is a stateful `Foo` widget. Its [State] uses the
128/// [SingleTickerProviderStateMixin] to implement the necessary
129/// [TickerProvider], creating its controller in the [State.initState] method
130/// and disposing of it in the [State.dispose] method. The duration of the
131/// controller is configured from a property in the `Foo` widget; as that
132/// changes, the [State.didUpdateWidget] method is used to update the
133/// controller.
134///
135/// ```dart
136/// class Foo extends StatefulWidget {
137/// const Foo({ super.key, required this.duration });
138///
139/// final Duration duration;
140///
141/// @override
142/// State<Foo> createState() => _FooState();
143/// }
144///
145/// class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
146/// late AnimationController _controller;
147///
148/// @override
149/// void initState() {
150/// super.initState();
151/// _controller = AnimationController(
152/// vsync: this, // the SingleTickerProviderStateMixin
153/// duration: widget.duration,
154/// );
155/// }
156///
157/// @override
158/// void didUpdateWidget(Foo oldWidget) {
159/// super.didUpdateWidget(oldWidget);
160/// _controller.duration = widget.duration;
161/// }
162///
163/// @override
164/// void dispose() {
165/// _controller.dispose();
166/// super.dispose();
167/// }
168///
169/// @override
170/// Widget build(BuildContext context) {
171/// return Container(); // ...
172/// }
173/// }
174/// ```
175/// {@end-tool}
176/// {@tool snippet}
177///
178/// The following method (for a [State] subclass) drives two animation
179/// controllers using Dart's asynchronous syntax for awaiting [Future] objects:
180///
181/// ```dart
182/// Future<void> fadeOutAndUpdateState() async {
183/// try {
184/// await fadeAnimationController.forward().orCancel;
185/// await sizeAnimationController.forward().orCancel;
186/// setState(() {
187/// dismissed = true;
188/// });
189/// } on TickerCanceled {
190/// // the animation got canceled, probably because we were disposed
191/// }
192/// }
193/// ```
194/// {@end-tool}
195///
196/// The assumption in the code above is that the animation controllers are being
197/// disposed in the [State] subclass' override of the [State.dispose] method.
198/// Since disposing the controller cancels the animation (raising a
199/// [TickerCanceled] exception), the code here can skip verifying whether
200/// [State.mounted] is still true at each step. (Again, this assumes that the
201/// controllers are created in [State.initState] and disposed in
202/// [State.dispose], as described in the previous section.)
203///
204/// {@tool dartpad}
205/// This example shows how to use [AnimationController] and
206/// [SlideTransition] to create an animated digit like you might find
207/// on an old pinball machine our your car's odometer. New digit
208/// values slide into place from below, as the old value slides
209/// upwards and out of view. Taps that occur while the controller is
210/// already animating cause the controller's
211/// [AnimationController.duration] to be reduced so that the visuals
212/// don't fall behind.
213///
214/// ** See code in examples/api/lib/animation/animation_controller/animated_digit.0.dart **
215/// {@end-tool}
216///
217/// See also:
218///
219/// * [Tween], the base class for converting an [AnimationController] to a
220/// range of values of other types.
221class AnimationController extends Animation<double>
222 with
223 AnimationEagerListenerMixin,
224 AnimationLocalListenersMixin,
225 AnimationLocalStatusListenersMixin {
226 /// Creates an animation controller.
227 ///
228 /// * `value` is the initial value of the animation. If defaults to the lower
229 /// bound.
230 ///
231 /// * [duration] is the length of time this animation should last.
232 ///
233 /// * [debugLabel] is a string to help identify this animation during
234 /// debugging (used by [toString]).
235 ///
236 /// * [lowerBound] is the smallest value this animation can obtain and the
237 /// value at which this animation is deemed to be dismissed.
238 ///
239 /// * [upperBound] is the largest value this animation can obtain and the
240 /// value at which this animation is deemed to be completed.
241 ///
242 /// * `vsync` is the required [TickerProvider] for the current context. It can
243 /// be changed by calling [resync]. See [TickerProvider] for advice on
244 /// obtaining a ticker provider.
245 AnimationController({
246 double? value,
247 this.duration,
248 this.reverseDuration,
249 this.debugLabel,
250 this.lowerBound = 0.0,
251 this.upperBound = 1.0,
252 this.animationBehavior = AnimationBehavior.normal,
253 required TickerProvider vsync,
254 }) : assert(upperBound >= lowerBound),
255 _direction = _AnimationDirection.forward {
256 assert(debugMaybeDispatchCreated('animation', 'AnimationController', this));
257 _ticker = vsync.createTicker(_tick);
258 _internalSetValue(value ?? lowerBound);
259 }
260
261 /// Creates an animation controller with no upper or lower bound for its
262 /// value.
263 ///
264 /// * [value] is the initial value of the animation.
265 ///
266 /// * [duration] is the length of time this animation should last.
267 ///
268 /// * [debugLabel] is a string to help identify this animation during
269 /// debugging (used by [toString]).
270 ///
271 /// * `vsync` is the required [TickerProvider] for the current context. It can
272 /// be changed by calling [resync]. See [TickerProvider] for advice on
273 /// obtaining a ticker provider.
274 ///
275 /// This constructor is most useful for animations that will be driven using a
276 /// physics simulation, especially when the physics simulation has no
277 /// pre-determined bounds.
278 AnimationController.unbounded({
279 double value = 0.0,
280 this.duration,
281 this.reverseDuration,
282 this.debugLabel,
283 required TickerProvider vsync,
284 this.animationBehavior = AnimationBehavior.preserve,
285 }) : lowerBound = double.negativeInfinity,
286 upperBound = double.infinity,
287 _direction = _AnimationDirection.forward {
288 assert(debugMaybeDispatchCreated('animation', 'AnimationController', this));
289 _ticker = vsync.createTicker(_tick);
290 _internalSetValue(value);
291 }
292
293 /// The value at which this animation is deemed to be dismissed.
294 final double lowerBound;
295
296 /// The value at which this animation is deemed to be completed.
297 final double upperBound;
298
299 /// A label that is used in the [toString] output. Intended to aid with
300 /// identifying animation controller instances in debug output.
301 final String? debugLabel;
302
303 /// The behavior of the controller when [AccessibilityFeatures.disableAnimations]
304 /// is true.
305 ///
306 /// Defaults to [AnimationBehavior.normal] for the [AnimationController.new]
307 /// constructor, and [AnimationBehavior.preserve] for the
308 /// [AnimationController.unbounded] constructor.
309 final AnimationBehavior animationBehavior;
310
311 /// Returns an [Animation<double>] for this animation controller, so that a
312 /// pointer to this object can be passed around without allowing users of that
313 /// pointer to mutate the [AnimationController] state.
314 Animation<double> get view => this;
315
316 /// The length of time this animation should last.
317 ///
318 /// If [reverseDuration] is specified, then [duration] is only used when going
319 /// [forward]. Otherwise, it specifies the duration going in both directions.
320 Duration? duration;
321
322 /// The length of time this animation should last when going in [reverse].
323 ///
324 /// The value of [duration] is used if [reverseDuration] is not specified or
325 /// set to null.
326 Duration? reverseDuration;
327
328 Ticker? _ticker;
329
330 /// Recreates the [Ticker] with the new [TickerProvider].
331 void resync(TickerProvider vsync) {
332 final Ticker oldTicker = _ticker!;
333 _ticker = vsync.createTicker(_tick);
334 _ticker!.absorbTicker(oldTicker);
335 }
336
337 Simulation? _simulation;
338
339 /// The current value of the animation.
340 ///
341 /// Setting this value notifies all the listeners that the value
342 /// changed.
343 ///
344 /// Setting this value also stops the controller if it is currently
345 /// running; if this happens, it also notifies all the status
346 /// listeners.
347 @override
348 double get value => _value;
349 late double _value;
350
351 /// Stops the animation controller and sets the current value of the
352 /// animation.
353 ///
354 /// The new value is clamped to the range set by [lowerBound] and
355 /// [upperBound].
356 ///
357 /// Value listeners are notified even if this does not change the value.
358 /// Status listeners are notified if the animation was previously playing.
359 ///
360 /// The most recently returned [TickerFuture], if any, is marked as having been
361 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
362 /// derivative future completes with a [TickerCanceled] error.
363 ///
364 /// See also:
365 ///
366 /// * [reset], which is equivalent to setting [value] to [lowerBound].
367 /// * [stop], which aborts the animation without changing its value or status
368 /// and without dispatching any notifications other than completing or
369 /// canceling the [TickerFuture].
370 /// * [forward], [reverse], [animateTo], [animateWith], [animateBackWith],
371 /// [fling], and [repeat], which start the animation controller.
372 set value(double newValue) {
373 stop();
374 _internalSetValue(newValue);
375 notifyListeners();
376 _checkStatusChanged();
377 }
378
379 /// Sets the controller's value to [lowerBound], stopping the animation (if
380 /// in progress), and resetting to its beginning point, or dismissed state.
381 ///
382 /// The most recently returned [TickerFuture], if any, is marked as having been
383 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
384 /// derivative future completes with a [TickerCanceled] error.
385 ///
386 /// See also:
387 ///
388 /// * [value], which can be explicitly set to a specific value as desired.
389 /// * [forward], which starts the animation in the forward direction.
390 /// * [stop], which aborts the animation without changing its value or status
391 /// and without dispatching any notifications other than completing or
392 /// canceling the [TickerFuture].
393 void reset() {
394 value = lowerBound;
395 }
396
397 /// The rate of change of [value] per second.
398 ///
399 /// If [isAnimating] is false, then [value] is not changing and the rate of
400 /// change is zero.
401 double get velocity {
402 if (!isAnimating) {
403 return 0.0;
404 }
405 return _simulation!.dx(
406 lastElapsedDuration!.inMicroseconds.toDouble() / Duration.microsecondsPerSecond,
407 );
408 }
409
410 void _internalSetValue(double newValue) {
411 _value = clampDouble(newValue, lowerBound, upperBound);
412 if (_value == lowerBound) {
413 _status = AnimationStatus.dismissed;
414 } else if (_value == upperBound) {
415 _status = AnimationStatus.completed;
416 } else {
417 _status = switch (_direction) {
418 _AnimationDirection.forward => AnimationStatus.forward,
419 _AnimationDirection.reverse => AnimationStatus.reverse,
420 };
421 }
422 }
423
424 /// The amount of time that has passed between the time the animation started
425 /// and the most recent tick of the animation.
426 ///
427 /// If the controller is not animating, the last elapsed duration is null.
428 Duration? get lastElapsedDuration => _lastElapsedDuration;
429 Duration? _lastElapsedDuration;
430
431 /// Whether this animation is currently animating in either the forward or reverse direction.
432 ///
433 /// This is separate from whether it is actively ticking. An animation
434 /// controller's ticker might get muted, in which case the animation
435 /// controller's callbacks will no longer fire even though time is continuing
436 /// to pass. See [Ticker.muted] and [TickerMode].
437 ///
438 /// If the animation was stopped (e.g. with [stop] or by setting a new [value]),
439 /// [isAnimating] will return `false` but the [status] will not change,
440 /// so the value of [AnimationStatus.isAnimating] might still be `true`.
441 @override
442 bool get isAnimating => _ticker != null && _ticker!.isActive;
443
444 _AnimationDirection _direction;
445
446 @override
447 AnimationStatus get status => _status;
448 late AnimationStatus _status;
449
450 /// Starts running this animation forwards (towards the end).
451 ///
452 /// Returns a [TickerFuture] that completes when the animation is complete.
453 ///
454 /// If [from] is non-null, it will be set as the current [value] before running
455 /// the animation.
456 ///
457 /// The most recently returned [TickerFuture], if any, is marked as having been
458 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
459 /// derivative future completes with a [TickerCanceled] error.
460 ///
461 /// During the animation, [status] is reported as [AnimationStatus.forward],
462 /// which switches to [AnimationStatus.completed] when [upperBound] is
463 /// reached at the end of the animation.
464 TickerFuture forward({double? from}) {
465 assert(() {
466 if (duration == null) {
467 throw FlutterError(
468 'AnimationController.forward() called with no default duration.\n'
469 'The "duration" property should be set, either in the constructor or later, before '
470 'calling the forward() function.',
471 );
472 }
473 return true;
474 }());
475 assert(
476 _ticker != null,
477 'AnimationController.forward() called after AnimationController.dispose()\n'
478 'AnimationController methods should not be used after calling dispose.',
479 );
480 _direction = _AnimationDirection.forward;
481 if (from != null) {
482 value = from;
483 }
484 return _animateToInternal(upperBound);
485 }
486
487 /// Starts running this animation in reverse (towards the beginning).
488 ///
489 /// Returns a [TickerFuture] that completes when the animation is dismissed.
490 ///
491 /// If [from] is non-null, it will be set as the current [value] before running
492 /// the animation.
493 ///
494 /// The most recently returned [TickerFuture], if any, is marked as having been
495 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
496 /// derivative future completes with a [TickerCanceled] error.
497 ///
498 /// During the animation, [status] is reported as [AnimationStatus.reverse],
499 /// which switches to [AnimationStatus.dismissed] when [lowerBound] is
500 /// reached at the end of the animation.
501 TickerFuture reverse({double? from}) {
502 assert(() {
503 if (duration == null && reverseDuration == null) {
504 throw FlutterError(
505 'AnimationController.reverse() called with no default duration or reverseDuration.\n'
506 'The "duration" or "reverseDuration" property should be set, either in the constructor or later, before '
507 'calling the reverse() function.',
508 );
509 }
510 return true;
511 }());
512 assert(
513 _ticker != null,
514 'AnimationController.reverse() called after AnimationController.dispose()\n'
515 'AnimationController methods should not be used after calling dispose.',
516 );
517 _direction = _AnimationDirection.reverse;
518 if (from != null) {
519 value = from;
520 }
521 return _animateToInternal(lowerBound);
522 }
523
524 /// Toggles the direction of this animation, based on whether it [isForwardOrCompleted].
525 ///
526 /// Specifically, this function acts the same way as [reverse] if the [status] is
527 /// either [AnimationStatus.forward] or [AnimationStatus.completed], and acts as
528 /// [forward] for [AnimationStatus.reverse] or [AnimationStatus.dismissed].
529 ///
530 /// If [from] is non-null, it will be set as the current [value] before running
531 /// the animation.
532 ///
533 /// The most recently returned [TickerFuture], if any, is marked as having been
534 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
535 /// derivative future completes with a [TickerCanceled] error.
536 TickerFuture toggle({double? from}) {
537 assert(() {
538 Duration? duration = this.duration;
539 if (isForwardOrCompleted) {
540 duration ??= reverseDuration;
541 }
542 if (duration == null) {
543 throw FlutterError(
544 'AnimationController.toggle() called with no default duration.\n'
545 'The "duration" property should be set, either in the constructor or later, before '
546 'calling the toggle() function.',
547 );
548 }
549 return true;
550 }());
551 assert(
552 _ticker != null,
553 'AnimationController.toggle() called after AnimationController.dispose()\n'
554 'AnimationController methods should not be used after calling dispose.',
555 );
556 _direction = isForwardOrCompleted ? _AnimationDirection.reverse : _AnimationDirection.forward;
557 if (from != null) {
558 value = from;
559 }
560 return _animateToInternal(switch (_direction) {
561 _AnimationDirection.forward => upperBound,
562 _AnimationDirection.reverse => lowerBound,
563 });
564 }
565
566 /// Drives the animation from its current value to the given target, "forward".
567 ///
568 /// Returns a [TickerFuture] that completes when the animation is complete.
569 ///
570 /// The most recently returned [TickerFuture], if any, is marked as having been
571 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
572 /// derivative future completes with a [TickerCanceled] error.
573 ///
574 /// During the animation, [status] is reported as [AnimationStatus.forward]
575 /// regardless of whether `target` > [value] or not. At the end of the
576 /// animation, when `target` is reached, [status] is reported as
577 /// [AnimationStatus.completed].
578 ///
579 /// If the `target` argument is the same as the current [value] of the
580 /// animation, then this won't animate, and the returned [TickerFuture] will
581 /// be already complete.
582 TickerFuture animateTo(double target, {Duration? duration, Curve curve = Curves.linear}) {
583 assert(() {
584 if (this.duration == null && duration == null) {
585 throw FlutterError(
586 'AnimationController.animateTo() called with no explicit duration and no default duration.\n'
587 'Either the "duration" argument to the animateTo() method should be provided, or the '
588 '"duration" property should be set, either in the constructor or later, before '
589 'calling the animateTo() function.',
590 );
591 }
592 return true;
593 }());
594 assert(
595 _ticker != null,
596 'AnimationController.animateTo() called after AnimationController.dispose()\n'
597 'AnimationController methods should not be used after calling dispose.',
598 );
599 _direction = _AnimationDirection.forward;
600 return _animateToInternal(target, duration: duration, curve: curve);
601 }
602
603 /// Drives the animation from its current value to the given target, "backward".
604 ///
605 /// Returns a [TickerFuture] that completes when the animation is complete.
606 ///
607 /// The most recently returned [TickerFuture], if any, is marked as having been
608 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
609 /// derivative future completes with a [TickerCanceled] error.
610 ///
611 /// During the animation, [status] is reported as [AnimationStatus.reverse]
612 /// regardless of whether `target` < [value] or not. At the end of the
613 /// animation, when `target` is reached, [status] is reported as
614 /// [AnimationStatus.dismissed].
615 ///
616 /// If the `target` argument is the same as the current [value] of the
617 /// animation, then this won't animate, and the returned [TickerFuture] will
618 /// be already complete.
619 TickerFuture animateBack(double target, {Duration? duration, Curve curve = Curves.linear}) {
620 assert(() {
621 if (this.duration == null && reverseDuration == null && duration == null) {
622 throw FlutterError(
623 'AnimationController.animateBack() called with no explicit duration and no default duration or reverseDuration.\n'
624 'Either the "duration" argument to the animateBack() method should be provided, or the '
625 '"duration" or "reverseDuration" property should be set, either in the constructor or later, before '
626 'calling the animateBack() function.',
627 );
628 }
629 return true;
630 }());
631 assert(
632 _ticker != null,
633 'AnimationController.animateBack() called after AnimationController.dispose()\n'
634 'AnimationController methods should not be used after calling dispose.',
635 );
636 _direction = _AnimationDirection.reverse;
637 return _animateToInternal(target, duration: duration, curve: curve);
638 }
639
640 TickerFuture _animateToInternal(
641 double target, {
642 Duration? duration,
643 Curve curve = Curves.linear,
644 }) {
645 final double scale = switch (animationBehavior) {
646 // Since the framework cannot handle zero duration animations, we run it at 5% of the normal
647 // duration to limit most animations to a single frame.
648 // Ideally, the framework would be able to handle zero duration animations, however, the common
649 // pattern of an eternally repeating animation might cause an endless loop if it weren't delayed
650 // for at least one frame.
651 AnimationBehavior.normal when SemanticsBinding.instance.disableAnimations => 0.05,
652 AnimationBehavior.normal || AnimationBehavior.preserve => 1.0,
653 };
654 Duration? simulationDuration = duration;
655 if (simulationDuration == null) {
656 assert(!(this.duration == null && _direction == _AnimationDirection.forward));
657 assert(
658 !(this.duration == null &&
659 _direction == _AnimationDirection.reverse &&
660 reverseDuration == null),
661 );
662 final double range = upperBound - lowerBound;
663 final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
664 final Duration directionDuration =
665 (_direction == _AnimationDirection.reverse && reverseDuration != null)
666 ? reverseDuration!
667 : this.duration!;
668 simulationDuration = directionDuration * remainingFraction;
669 } else if (target == value) {
670 // Already at target, don't animate.
671 simulationDuration = Duration.zero;
672 }
673 stop();
674 if (simulationDuration == Duration.zero) {
675 if (value != target) {
676 _value = clampDouble(target, lowerBound, upperBound);
677 notifyListeners();
678 }
679 _status =
680 (_direction == _AnimationDirection.forward)
681 ? AnimationStatus.completed
682 : AnimationStatus.dismissed;
683 _checkStatusChanged();
684 return TickerFuture.complete();
685 }
686 assert(simulationDuration > Duration.zero);
687 assert(!isAnimating);
688 return _startSimulation(
689 _InterpolationSimulation(_value, target, simulationDuration, curve, scale),
690 );
691 }
692
693 /// Starts running this animation in the forward direction, and
694 /// restarts the animation when it completes.
695 ///
696 /// Defaults to repeating between the [lowerBound] and [upperBound] of the
697 /// [AnimationController] when no explicit value is set for [min] and [max].
698 ///
699 /// With [reverse] set to true, instead of always starting over at [min]
700 /// the starting value will alternate between [min] and [max] values on each
701 /// repeat. The [status] will be reported as [AnimationStatus.reverse] when
702 /// the animation runs from [max] to [min].
703 ///
704 /// Each run of the animation will have a duration of `period`. If `period` is not
705 /// provided, [duration] will be used instead, which has to be set before [repeat] is
706 /// called either in the constructor or later by using the [duration] setter.
707 ///
708 /// If a value is passed to [count], the animation will perform that many
709 /// iterations before stopping. Otherwise, the animation repeats indefinitely.
710 ///
711 /// Returns a [TickerFuture] that never completes, unless a [count] is specified.
712 /// The [TickerFuture.orCancel] future completes with an error when the animation is
713 /// stopped (e.g. with [stop]).
714 ///
715 /// The most recently returned [TickerFuture], if any, is marked as having been
716 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
717 /// derivative future completes with a [TickerCanceled] error.
718 TickerFuture repeat({
719 double? min,
720 double? max,
721 bool reverse = false,
722 Duration? period,
723 int? count,
724 }) {
725 min ??= lowerBound;
726 max ??= upperBound;
727 period ??= duration;
728 assert(() {
729 if (period == null) {
730 throw FlutterError(
731 'AnimationController.repeat() called without an explicit period and with no default Duration.\n'
732 'Either the "period" argument to the repeat() method should be provided, or the '
733 '"duration" property should be set, either in the constructor or later, before '
734 'calling the repeat() function.',
735 );
736 }
737 return true;
738 }());
739 assert(max >= min);
740 assert(max <= upperBound && min >= lowerBound);
741 assert(count == null || count > 0, 'Count shall be greater than zero if not null');
742 stop();
743 return _startSimulation(
744 _RepeatingSimulation(_value, min, max, reverse, period!, _directionSetter, count),
745 );
746 }
747
748 void _directionSetter(_AnimationDirection direction) {
749 _direction = direction;
750 _status =
751 (_direction == _AnimationDirection.forward)
752 ? AnimationStatus.forward
753 : AnimationStatus.reverse;
754 _checkStatusChanged();
755 }
756
757 /// Drives the animation with a spring (within [lowerBound] and [upperBound])
758 /// and initial velocity.
759 ///
760 /// If velocity is positive, the animation will complete, otherwise it will
761 /// dismiss. The velocity is specified in units per second. If the
762 /// [SemanticsBinding.disableAnimations] flag is set, the velocity is somewhat
763 /// arbitrarily multiplied by 200.
764 ///
765 /// The [springDescription] parameter can be used to specify a custom
766 /// [SpringType.criticallyDamped] or [SpringType.overDamped] spring with which
767 /// to drive the animation. By default, a [SpringType.criticallyDamped] spring
768 /// is used. See [SpringDescription.withDampingRatio] for how to create a
769 /// suitable [SpringDescription].
770 ///
771 /// The resulting spring simulation cannot be of type [SpringType.underDamped];
772 /// such a spring would oscillate rather than fling.
773 ///
774 /// Returns a [TickerFuture] that completes when the animation is complete.
775 ///
776 /// The most recently returned [TickerFuture], if any, is marked as having been
777 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
778 /// derivative future completes with a [TickerCanceled] error.
779 TickerFuture fling({
780 double velocity = 1.0,
781 SpringDescription? springDescription,
782 AnimationBehavior? animationBehavior,
783 }) {
784 springDescription ??= _kFlingSpringDescription;
785 _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
786 final double target =
787 velocity < 0.0
788 ? lowerBound - _kFlingTolerance.distance
789 : upperBound + _kFlingTolerance.distance;
790 final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
791 final double scale = switch (behavior) {
792 // This is arbitrary (it was chosen because it worked for the drawer widget).
793 AnimationBehavior.normal when SemanticsBinding.instance.disableAnimations => 200.0,
794 AnimationBehavior.normal || AnimationBehavior.preserve => 1.0,
795 };
796 final SpringSimulation simulation = SpringSimulation(
797 springDescription,
798 value,
799 target,
800 velocity * scale,
801 )..tolerance = _kFlingTolerance;
802 assert(
803 simulation.type != SpringType.underDamped,
804 'The specified spring simulation is of type SpringType.underDamped.\n'
805 'An underdamped spring results in oscillation rather than a fling. '
806 'Consider specifying a different springDescription, or use animateWith() '
807 'with an explicit SpringSimulation if an underdamped spring is intentional.',
808 );
809 stop();
810 return _startSimulation(simulation);
811 }
812
813 /// Drives the animation according to the given simulation.
814 ///
815 /// {@template flutter.animation.AnimationController.animateWith}
816 /// The values from the simulation are clamped to the [lowerBound] and
817 /// [upperBound]. To avoid this, consider creating the [AnimationController]
818 /// using the [AnimationController.unbounded] constructor.
819 ///
820 /// Returns a [TickerFuture] that completes when the animation is complete.
821 ///
822 /// The most recently returned [TickerFuture], if any, is marked as having been
823 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
824 /// derivative future completes with a [TickerCanceled] error.
825 /// {@endtemplate}
826 ///
827 /// The [status] is always [AnimationStatus.forward] for the entire duration
828 /// of the simulation.
829 ///
830 /// See also:
831 ///
832 /// * [animateBackWith], which is like this method but the status is always
833 /// [AnimationStatus.reverse].
834 TickerFuture animateWith(Simulation simulation) {
835 assert(
836 _ticker != null,
837 'AnimationController.animateWith() called after AnimationController.dispose()\n'
838 'AnimationController methods should not be used after calling dispose.',
839 );
840 stop();
841 _direction = _AnimationDirection.forward;
842 return _startSimulation(simulation);
843 }
844
845 /// Drives the animation according to the given simulation with a [status] of
846 /// [AnimationStatus.reverse].
847 ///
848 /// {@macro flutter.animation.AnimationController.animateWith}
849 ///
850 /// The [status] is always [AnimationStatus.reverse] for the entire duration
851 /// of the simulation.
852 ///
853 /// See also:
854 ///
855 /// * [animateWith], which is like this method but the status is always
856 /// [AnimationStatus.forward].
857 TickerFuture animateBackWith(Simulation simulation) {
858 assert(
859 _ticker != null,
860 'AnimationController.animateWith() called after AnimationController.dispose()\n'
861 'AnimationController methods should not be used after calling dispose.',
862 );
863 stop();
864 _direction = _AnimationDirection.reverse;
865 return _startSimulation(simulation);
866 }
867
868 TickerFuture _startSimulation(Simulation simulation) {
869 assert(!isAnimating);
870 _simulation = simulation;
871 _lastElapsedDuration = Duration.zero;
872 _value = clampDouble(simulation.x(0.0), lowerBound, upperBound);
873 final TickerFuture result = _ticker!.start();
874 _status =
875 (_direction == _AnimationDirection.forward)
876 ? AnimationStatus.forward
877 : AnimationStatus.reverse;
878 _checkStatusChanged();
879 return result;
880 }
881
882 /// Stops running this animation.
883 ///
884 /// This does not trigger any notifications. The animation stops in its
885 /// current state.
886 ///
887 /// By default, the most recently returned [TickerFuture] is marked as having
888 /// been canceled, meaning the future never completes and its
889 /// [TickerFuture.orCancel] derivative future completes with a [TickerCanceled]
890 /// error. By passing the `canceled` argument with the value false, this is
891 /// reversed, and the futures complete successfully.
892 ///
893 /// See also:
894 ///
895 /// * [reset], which stops the animation and resets it to the [lowerBound],
896 /// and which does send notifications.
897 /// * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat],
898 /// which restart the animation controller.
899 void stop({bool canceled = true}) {
900 assert(
901 _ticker != null,
902 'AnimationController.stop() called after AnimationController.dispose()\n'
903 'AnimationController methods should not be used after calling dispose.',
904 );
905 _simulation = null;
906 _lastElapsedDuration = null;
907 _ticker!.stop(canceled: canceled);
908 }
909
910 /// Release the resources used by this object. The object is no longer usable
911 /// after this method is called.
912 ///
913 /// The most recently returned [TickerFuture], if any, is marked as having been
914 /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
915 /// derivative future completes with a [TickerCanceled] error.
916 @override
917 void dispose() {
918 assert(() {
919 if (_ticker == null) {
920 throw FlutterError.fromParts(<DiagnosticsNode>[
921 ErrorSummary('AnimationController.dispose() called more than once.'),
922 ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
923 DiagnosticsProperty<AnimationController>(
924 'The following $runtimeType object was disposed multiple times',
925 this,
926 style: DiagnosticsTreeStyle.errorProperty,
927 ),
928 ]);
929 }
930 return true;
931 }());
932 assert(debugMaybeDispatchDisposed(this));
933 _ticker!.dispose();
934 _ticker = null;
935 clearStatusListeners();
936 clearListeners();
937 super.dispose();
938 }
939
940 AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
941 void _checkStatusChanged() {
942 final AnimationStatus newStatus = status;
943 if (_lastReportedStatus != newStatus) {
944 _lastReportedStatus = newStatus;
945 notifyStatusListeners(newStatus);
946 }
947 }
948
949 void _tick(Duration elapsed) {
950 _lastElapsedDuration = elapsed;
951 final double elapsedInSeconds =
952 elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
953 assert(elapsedInSeconds >= 0.0);
954 _value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound);
955 if (_simulation!.isDone(elapsedInSeconds)) {
956 _status =
957 (_direction == _AnimationDirection.forward)
958 ? AnimationStatus.completed
959 : AnimationStatus.dismissed;
960 stop(canceled: false);
961 }
962 notifyListeners();
963 _checkStatusChanged();
964 }
965
966 @override
967 String toStringDetails() {
968 final String paused = isAnimating ? '' : '; paused';
969 final String ticker = _ticker == null ? '; DISPOSED' : (_ticker!.muted ? '; silenced' : '');
970 String label = '';
971 assert(() {
972 if (debugLabel != null) {
973 label = '; for $debugLabel';
974 }
975 return true;
976 }());
977 final String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
978 return '$more$paused$ticker$label';
979 }
980}
981
982class _InterpolationSimulation extends Simulation {
983 _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale)
984 : assert(duration.inMicroseconds > 0),
985 _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
986
987 final double _durationInSeconds;
988 final double _begin;
989 final double _end;
990 final Curve _curve;
991
992 @override
993 double x(double timeInSeconds) {
994 final double t = clampDouble(timeInSeconds / _durationInSeconds, 0.0, 1.0);
995 return switch (t) {
996 0.0 => _begin,
997 1.0 => _end,
998 _ => _begin + (_end - _begin) * _curve.transform(t),
999 };
1000 }
1001
1002 @override
1003 double dx(double timeInSeconds) {
1004 final double epsilon = tolerance.time;
1005 return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
1006 }
1007
1008 @override
1009 bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
1010}
1011
1012typedef _DirectionSetter = void Function(_AnimationDirection direction);
1013
1014class _RepeatingSimulation extends Simulation {
1015 _RepeatingSimulation(
1016 double initialValue,
1017 this.min,
1018 this.max,
1019 this.reverse,
1020 Duration period,
1021 this.directionSetter,
1022 this.count,
1023 ) : assert(count == null || count > 0, 'Count shall be greater than zero if not null'),
1024 _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
1025 _initialT =
1026 (max == min)
1027 ? 0.0
1028 : ((clampDouble(initialValue, min, max) - min) / (max - min)) *
1029 (period.inMicroseconds / Duration.microsecondsPerSecond) {
1030 assert(_periodInSeconds > 0.0);
1031 assert(_initialT >= 0.0);
1032 }
1033
1034 final double min;
1035 final double max;
1036 final bool reverse;
1037 final int? count;
1038 final _DirectionSetter directionSetter;
1039
1040 final double _periodInSeconds;
1041 final double _initialT;
1042
1043 late final double _exitTimeInSeconds = (count! * _periodInSeconds) - _initialT;
1044
1045 @override
1046 double x(double timeInSeconds) {
1047 assert(timeInSeconds >= 0.0);
1048
1049 final double totalTimeInSeconds = timeInSeconds + _initialT;
1050 final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0;
1051 final bool isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds).isOdd;
1052
1053 if (reverse && isPlayingReverse) {
1054 directionSetter(_AnimationDirection.reverse);
1055 return ui.lerpDouble(max, min, t)!;
1056 } else {
1057 directionSetter(_AnimationDirection.forward);
1058 return ui.lerpDouble(min, max, t)!;
1059 }
1060 }
1061
1062 @override
1063 double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
1064
1065 @override
1066 bool isDone(double timeInSeconds) {
1067 // if [timeInSeconds] elapsed the [_exitTimeInSeconds] && [count] is not null,
1068 // consider marking the simulation as "DONE"
1069 return count != null && (timeInSeconds >= _exitTimeInSeconds);
1070 }
1071}
1072