| // Copyright 2014 The Flutter 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'; |
| import 'dart:ui' as ui show lerpDouble; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/physics.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/semantics.dart'; |
| |
| import 'animation.dart'; |
| import 'curves.dart'; |
| import 'listener_helpers.dart'; |
| |
| export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled; |
| |
| // Examples can assume: |
| // AnimationController _controller, fadeAnimationController, sizeAnimationController; |
| // bool dismissed; |
| // void setState(VoidCallback fn) { } |
| |
| /// 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. |
| reverse, |
| } |
| |
| final SpringDescription _kFlingSpringDescription = SpringDescription.withDampingRatio( |
| mass: 1.0, |
| stiffness: 500.0, |
| ratio: 1.0, |
| ); |
| |
| const Tolerance _kFlingTolerance = Tolerance( |
| velocity: double.infinity, |
| distance: 0.01, |
| ); |
| |
| /// Configures how an [AnimationController] behaves when animations are |
| /// disabled. |
| /// |
| /// When [AccessibilityFeatures.disableAnimations] is true, the device is asking |
| /// Flutter to reduce or disable animations as much as possible. To honor this, |
| /// 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. |
| /// |
| /// For example, the [AnimationController] which controls the physics simulation |
| /// for a scrollable list will have [AnimationBehavior.preserve], so that when |
| /// 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, |
| } |
| |
| /// A controller for an animation. |
| /// |
| /// 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. |
| /// |
| /// 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). |
| /// |
| /// ## 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] |
| /// |
| /// 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 the `fadeOutAndUpdateState` method |
| /// below. |
| /// |
| /// {@tool snippet} |
| /// |
| /// Here is a stateful `Foo` widget. Its [State] uses the |
| /// [SingleTickerProviderStateMixin] to implement the necessary |
| /// [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. |
| /// |
| /// ```dart |
| /// class Foo extends StatefulWidget { |
| /// Foo({ Key key, this.duration }) : super(key: key); |
| /// |
| /// final Duration duration; |
| /// |
| /// @override |
| /// _FooState createState() => _FooState(); |
| /// } |
| /// |
| /// class _FooState extends State<Foo> with SingleTickerProviderStateMixin { |
| /// AnimationController _controller; |
| /// |
| /// @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(); // ... |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// {@tool snippet} |
| /// |
| /// The following method (for a [State] subclass) drives two animation |
| /// controllers using Dart's asynchronous syntax for awaiting [Future] objects: |
| /// |
| /// ```dart |
| /// Future<void> 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 |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// 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.) |
| /// |
| /// See also: |
| /// |
| /// * [Tween], the base class for converting an [AnimationController] to a |
| /// range of values of other types. |
| class AnimationController extends Animation<double> |
| with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { |
| /// Creates an animation controller. |
| /// |
| /// * `value` is the initial value of the animation. If defaults to the lower |
| /// bound. |
| /// |
| /// * [duration] is the length of time this animation should last. |
| /// |
| /// * [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 |
| /// changed by calling [resync]. It is required and must not be null. See |
| /// [TickerProvider] for advice on obtaining a ticker provider. |
| AnimationController({ |
| double value, |
| this.duration, |
| this.reverseDuration, |
| this.debugLabel, |
| this.lowerBound = 0.0, |
| this.upperBound = 1.0, |
| this.animationBehavior = AnimationBehavior.normal, |
| @required TickerProvider vsync, |
| }) : assert(lowerBound != null), |
| assert(upperBound != null), |
| assert(upperBound >= lowerBound), |
| assert(vsync != null), |
| _direction = _AnimationDirection.forward { |
| _ticker = vsync.createTicker(_tick); |
| _internalSetValue(value ?? lowerBound); |
| } |
| |
| /// Creates an animation controller with no upper or lower bound for its |
| /// value. |
| /// |
| /// * [value] is the initial value of the animation. |
| /// |
| /// * [duration] is the length of time this animation should last. |
| /// |
| /// * [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 |
| /// changed by calling [resync]. It is required and must not be null. See |
| /// [TickerProvider] for advice on obtaining a ticker provider. |
| /// |
| /// This constructor is most useful for animations that will be driven using a |
| /// physics simulation, especially when the physics simulation has no |
| /// pre-determined bounds. |
| AnimationController.unbounded({ |
| double value = 0.0, |
| this.duration, |
| this.reverseDuration, |
| this.debugLabel, |
| @required TickerProvider vsync, |
| this.animationBehavior = AnimationBehavior.preserve, |
| }) : assert(value != null), |
| assert(vsync != null), |
| lowerBound = double.negativeInfinity, |
| upperBound = double.infinity, |
| _direction = _AnimationDirection.forward { |
| _ticker = vsync.createTicker(_tick); |
| _internalSetValue(value); |
| } |
| |
| /// 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; |
| |
| /// A label that is used in the [toString] output. Intended to aid with |
| /// identifying animation controller instances in debug output. |
| final String debugLabel; |
| |
| /// The behavior of the controller when [AccessibilityFeatures.disableAnimations] |
| /// is true. |
| /// |
| /// Defaults to [AnimationBehavior.normal] for the [new AnimationController] |
| /// constructor, and [AnimationBehavior.preserve] for the |
| /// [new AnimationController.unbounded] constructor. |
| final AnimationBehavior animationBehavior; |
| |
| /// 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. |
| Animation<double> get view => this; |
| |
| /// The length of time this animation should last. |
| /// |
| /// If [reverseDuration] is specified, then [duration] is only used when going |
| /// [forward]. Otherwise, it specifies the duration going in both directions. |
| Duration duration; |
| |
| /// The length of time this animation should last when going in [reverse]. |
| /// |
| /// The value of [duration] us used if [reverseDuration] is not specified or |
| /// set to null. |
| Duration reverseDuration; |
| |
| Ticker _ticker; |
| |
| /// Recreates the [Ticker] with the new [TickerProvider]. |
| void resync(TickerProvider vsync) { |
| final Ticker oldTicker = _ticker; |
| _ticker = vsync.createTicker(_tick); |
| _ticker.absorbTicker(oldTicker); |
| } |
| |
| Simulation _simulation; |
| |
| /// The current value of the animation. |
| /// |
| /// 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. |
| @override |
| double get value => _value; |
| double _value; |
| /// 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. |
| /// |
| /// 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: |
| /// |
| /// * [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. |
| set value(double newValue) { |
| assert(newValue != null); |
| stop(); |
| _internalSetValue(newValue); |
| notifyListeners(); |
| _checkStatusChanged(); |
| } |
| |
| /// Sets the controller's value to [lowerBound], stopping the animation (if |
| /// in progress), and resetting to its beginning point, or dismissed state. |
| /// |
| /// 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]. |
| void reset() { |
| value = lowerBound; |
| } |
| |
| /// The rate of change of [value] per second. |
| /// |
| /// If [isAnimating] is false, then [value] is not changing and the rate of |
| /// change is zero. |
| double get velocity { |
| if (!isAnimating) |
| return 0.0; |
| return _simulation.dx(lastElapsedDuration.inMicroseconds.toDouble() / Duration.microsecondsPerSecond); |
| } |
| |
| void _internalSetValue(double newValue) { |
| _value = newValue.clamp(lowerBound, upperBound) as double; |
| if (_value == lowerBound) { |
| _status = AnimationStatus.dismissed; |
| } else if (_value == upperBound) { |
| _status = AnimationStatus.completed; |
| } else { |
| _status = (_direction == _AnimationDirection.forward) ? |
| AnimationStatus.forward : |
| AnimationStatus.reverse; |
| } |
| } |
| |
| /// The amount of time that has passed between the time the animation started |
| /// and the most recent tick of the animation. |
| /// |
| /// If the controller is not animating, the last elapsed duration is null. |
| Duration get lastElapsedDuration => _lastElapsedDuration; |
| Duration _lastElapsedDuration; |
| |
| /// Whether this animation is currently animating in either the forward or reverse direction. |
| /// |
| /// 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]. |
| bool get isAnimating => _ticker != null && _ticker.isActive; |
| |
| _AnimationDirection _direction; |
| |
| @override |
| AnimationStatus get status => _status; |
| AnimationStatus _status; |
| |
| /// Starts running this animation forwards (towards the end). |
| /// |
| /// 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.forward], |
| /// which switches to [AnimationStatus.completed] when [upperBound] is |
| /// reached at the end of the animation. |
| TickerFuture forward({ double from }) { |
| assert(() { |
| if (duration == null) { |
| throw 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; |
| }()); |
| assert( |
| _ticker != null, |
| 'AnimationController.forward() called after AnimationController.dispose()\n' |
| 'AnimationController methods should not be used after calling dispose.' |
| ); |
| _direction = _AnimationDirection.forward; |
| if (from != null) |
| value = from; |
| return _animateToInternal(upperBound); |
| } |
| |
| /// Starts running this animation in reverse (towards the beginning). |
| /// |
| /// 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. |
| /// |
| /// During the animation, [status] is reported as [AnimationStatus.reverse], |
| /// which switches to [AnimationStatus.dismissed] when [lowerBound] is |
| /// reached at the end of the animation. |
| TickerFuture reverse({ double from }) { |
| assert(() { |
| if (duration == null && reverseDuration == null) { |
| throw FlutterError( |
| '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 ' |
| 'calling the reverse() function.' |
| ); |
| } |
| return true; |
| }()); |
| assert( |
| _ticker != null, |
| 'AnimationController.reverse() called after AnimationController.dispose()\n' |
| 'AnimationController methods should not be used after calling dispose.' |
| ); |
| _direction = _AnimationDirection.reverse; |
| if (from != null) |
| value = from; |
| return _animateToInternal(lowerBound); |
| } |
| |
| /// 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.forward] |
| /// regardless of whether `target` > [value] or not. At the end of the |
| /// animation, when `target` is reached, [status] is reported as |
| /// [AnimationStatus.completed]. |
| TickerFuture animateTo(double target, { Duration duration, Curve curve = Curves.linear }) { |
| assert( |
| _ticker != null, |
| 'AnimationController.animateTo() called after AnimationController.dispose()\n' |
| 'AnimationController methods should not be used after calling dispose.' |
| ); |
| _direction = _AnimationDirection.forward; |
| return _animateToInternal(target, duration: duration, curve: curve); |
| } |
| |
| /// 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]. |
| TickerFuture animateBack(double target, { Duration duration, Curve curve = Curves.linear }) { |
| assert( |
| _ticker != null, |
| 'AnimationController.animateBack() called after AnimationController.dispose()\n' |
| 'AnimationController methods should not be used after calling dispose.' |
| ); |
| _direction = _AnimationDirection.reverse; |
| return _animateToInternal(target, duration: duration, curve: curve); |
| } |
| |
| TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) { |
| double scale = 1.0; |
| if (SemanticsBinding.instance.disableAnimations) { |
| switch (animationBehavior) { |
| case AnimationBehavior.normal: |
| // 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. |
| // TODO(jonahwilliams): determine a better process for setting duration. |
| scale = 0.05; |
| break; |
| case AnimationBehavior.preserve: |
| break; |
| } |
| } |
| Duration simulationDuration = duration; |
| if (simulationDuration == null) { |
| assert(() { |
| if ((this.duration == null && _direction == _AnimationDirection.reverse && reverseDuration == null) || this.duration == null) { |
| throw FlutterError( |
| 'AnimationController.animateTo() called with no explicit duration and no default duration or reverseDuration.\n' |
| 'Either the "duration" argument to the animateTo() method should be provided, or the ' |
| '"duration" and/or "reverseDuration" property should be set, either in the constructor or later, before ' |
| 'calling the animateTo() function.' |
| ); |
| } |
| return true; |
| }()); |
| final double range = upperBound - lowerBound; |
| final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0; |
| final Duration directionDuration = |
| (_direction == _AnimationDirection.reverse && reverseDuration != null) |
| ? reverseDuration |
| : this.duration; |
| simulationDuration = directionDuration * remainingFraction; |
| } else if (target == value) { |
| // Already at target, don't animate. |
| simulationDuration = Duration.zero; |
| } |
| stop(); |
| if (simulationDuration == Duration.zero) { |
| if (value != target) { |
| _value = target.clamp(lowerBound, upperBound) as double; |
| notifyListeners(); |
| } |
| _status = (_direction == _AnimationDirection.forward) ? |
| AnimationStatus.completed : |
| AnimationStatus.dismissed; |
| _checkStatusChanged(); |
| return TickerFuture.complete(); |
| } |
| assert(simulationDuration > Duration.zero); |
| assert(!isAnimating); |
| return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale)); |
| } |
| |
| /// Starts running this animation in the forward direction, and |
| /// restarts the animation when it completes. |
| /// |
| /// 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] |
| /// 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]. |
| /// |
| /// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future |
| /// 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, bool reverse = false, Duration period }) { |
| min ??= lowerBound; |
| max ??= upperBound; |
| period ??= duration; |
| assert(() { |
| if (period == null) { |
| throw FlutterError( |
| '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 ' |
| '"duration" property should be set, either in the constructor or later, before ' |
| 'calling the repeat() function.' |
| ); |
| } |
| return true; |
| }()); |
| assert(max >= min); |
| assert(max <= upperBound && min >= lowerBound); |
| assert(reverse != null); |
| stop(); |
| return _startSimulation(_RepeatingSimulation(_value, min, max, reverse, period, _directionSetter)); |
| } |
| |
| void _directionSetter(_AnimationDirection direction) { |
| _direction = direction; |
| _status = (_direction == _AnimationDirection.forward) ? |
| AnimationStatus.forward : |
| AnimationStatus.reverse; |
| _checkStatusChanged(); |
| } |
| |
| /// 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. |
| /// |
| /// 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 fling({ double velocity = 1.0, AnimationBehavior animationBehavior }) { |
| _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward; |
| final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance |
| : upperBound + _kFlingTolerance.distance; |
| double scale = 1.0; |
| final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; |
| if (SemanticsBinding.instance.disableAnimations) { |
| switch (behavior) { |
| case AnimationBehavior.normal: |
| // TODO(jonahwilliams): determine a better process for setting velocity. |
| // the value below was arbitrarily chosen because it worked for the drawer widget. |
| scale = 200.0; |
| break; |
| case AnimationBehavior.preserve: |
| break; |
| } |
| } |
| final Simulation simulation = SpringSimulation(_kFlingSpringDescription, value, target, velocity * scale) |
| ..tolerance = _kFlingTolerance; |
| stop(); |
| return _startSimulation(simulation); |
| } |
| |
| /// Drives the animation according to the given simulation. |
| /// |
| /// The values from the simulation are clamped to the [lowerBound] and |
| /// [upperBound]. To avoid this, consider creating the [AnimationController] |
| /// using the [new AnimationController.unbounded] constructor. |
| /// |
| /// 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. |
| /// |
| /// The [status] is always [AnimationStatus.forward] for the entire duration |
| /// of the simulation. |
| TickerFuture animateWith(Simulation simulation) { |
| assert( |
| _ticker != null, |
| 'AnimationController.animateWith() called after AnimationController.dispose()\n' |
| 'AnimationController methods should not be used after calling dispose.' |
| ); |
| stop(); |
| _direction = _AnimationDirection.forward; |
| return _startSimulation(simulation); |
| } |
| |
| TickerFuture _startSimulation(Simulation simulation) { |
| assert(simulation != null); |
| assert(!isAnimating); |
| _simulation = simulation; |
| _lastElapsedDuration = Duration.zero; |
| _value = simulation.x(0.0).clamp(lowerBound, upperBound) as double; |
| final TickerFuture result = _ticker.start(); |
| _status = (_direction == _AnimationDirection.forward) ? |
| AnimationStatus.forward : |
| AnimationStatus.reverse; |
| _checkStatusChanged(); |
| return result; |
| } |
| |
| /// Stops running this animation. |
| /// |
| /// This does not trigger any notifications. The animation stops in its |
| /// current state. |
| /// |
| /// 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] |
| /// error. By passing the `canceled` argument with the value false, this is |
| /// reversed, and the futures complete successfully. |
| /// |
| /// 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. |
| void stop({ bool canceled = true }) { |
| assert( |
| _ticker != null, |
| 'AnimationController.stop() called after AnimationController.dispose()\n' |
| 'AnimationController methods should not be used after calling dispose.' |
| ); |
| _simulation = null; |
| _lastElapsedDuration = null; |
| _ticker.stop(canceled: canceled); |
| } |
| |
| /// Release the resources used by this object. The object is no longer usable |
| /// after this method is called. |
| /// |
| /// 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. |
| @override |
| void dispose() { |
| assert(() { |
| if (_ticker == null) { |
| 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, |
| ), |
| ]); |
| } |
| return true; |
| }()); |
| _ticker.dispose(); |
| _ticker = null; |
| super.dispose(); |
| } |
| |
| AnimationStatus _lastReportedStatus = AnimationStatus.dismissed; |
| void _checkStatusChanged() { |
| final AnimationStatus newStatus = status; |
| if (_lastReportedStatus != newStatus) { |
| _lastReportedStatus = newStatus; |
| notifyStatusListeners(newStatus); |
| } |
| } |
| |
| void _tick(Duration elapsed) { |
| _lastElapsedDuration = elapsed; |
| final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond; |
| assert(elapsedInSeconds >= 0.0); |
| _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound) as double; |
| if (_simulation.isDone(elapsedInSeconds)) { |
| _status = (_direction == _AnimationDirection.forward) ? |
| AnimationStatus.completed : |
| AnimationStatus.dismissed; |
| stop(canceled: false); |
| } |
| notifyListeners(); |
| _checkStatusChanged(); |
| } |
| |
| @override |
| String toStringDetails() { |
| 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)}'; |
| return '$more$paused$ticker$label'; |
| } |
| } |
| |
| class _InterpolationSimulation extends Simulation { |
| _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale) |
| : assert(_begin != null), |
| assert(_end != null), |
| assert(duration != null && duration.inMicroseconds > 0), |
| _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond; |
| |
| final double _durationInSeconds; |
| final double _begin; |
| final double _end; |
| final Curve _curve; |
| |
| @override |
| double x(double timeInSeconds) { |
| final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0) as double; |
| if (t == 0.0) |
| return _begin; |
| else if (t == 1.0) |
| return _end; |
| else |
| return _begin + (_end - _begin) * _curve.transform(t); |
| } |
| |
| @override |
| double dx(double timeInSeconds) { |
| final double epsilon = tolerance.time; |
| return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon); |
| } |
| |
| @override |
| bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds; |
| } |
| |
| typedef _DirectionSetter = void Function(_AnimationDirection direction); |
| |
| class _RepeatingSimulation extends Simulation { |
| _RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period, this.directionSetter) |
| : _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond, |
| _initialT = (max == min) ? 0.0 : (initialValue / (max - min)) * (period.inMicroseconds / Duration.microsecondsPerSecond) { |
| assert(_periodInSeconds > 0.0); |
| assert(_initialT >= 0.0); |
| } |
| |
| final double min; |
| final double max; |
| final bool reverse; |
| final _DirectionSetter directionSetter; |
| |
| final double _periodInSeconds; |
| final double _initialT; |
| |
| @override |
| double x(double timeInSeconds) { |
| assert(timeInSeconds >= 0.0); |
| |
| final double totalTimeInSeconds = timeInSeconds + _initialT; |
| final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0; |
| final bool _isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds) % 2 == 1; |
| |
| if (reverse && _isPlayingReverse) { |
| directionSetter(_AnimationDirection.reverse); |
| return ui.lerpDouble(max, min, t); |
| } else { |
| directionSetter(_AnimationDirection.forward); |
| return ui.lerpDouble(min, max, t); |
| } |
| } |
| |
| @override |
| double dx(double timeInSeconds) => (max - min) / _periodInSeconds; |
| |
| @override |
| bool isDone(double timeInSeconds) => false; |
| } |