| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:ui' as ui show lerpDouble; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/physics.dart'; |
| import 'package:flutter/scheduler.dart'; |
| |
| import 'animation.dart'; |
| import 'curves.dart'; |
| import 'listener_helpers.dart'; |
| |
| export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled; |
| |
| // Examples can assume: |
| // AnimationController _controller; |
| |
| /// 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 = new SpringDescription.withDampingRatio( |
| mass: 1.0, |
| stiffness: 500.0, |
| ratio: 1.0, |
| ); |
| |
| const Tolerance _kFlingTolerance = const Tolerance( |
| velocity: double.INFINITY, |
| distance: 0.01, |
| ); |
| |
| /// 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). |
| /// |
| /// An AnimationController needs a [TickerProvider], which is configured using |
| /// the `vsync` argument on the constructor. If you are creating an |
| /// AnimationController from a [State], then you can use the |
| /// [TickerProviderStateMixin] and [SingleTickerProviderStateMixin] classes to |
| /// obtain a suitable [TickerProvider]. The widget test framework [WidgetTester] |
| /// object can be used as a ticker provider in the context of tests. In other |
| /// contexts, you will have to either pass a [TickerProvider] from a higher |
| /// level (e.g. indirectly from a [State] that mixes in |
| /// [TickerProviderStateMixin]), or create a custom [TickerProvider] subclass. |
| /// |
| /// The methods that start animations return a [TickerFuture] object which |
| /// completes when the animation completes successfully, and never throws an |
| /// error; if the animation is canceled, the future never completes. This object |
| /// also has a [TickerFuture.orCancel] property which returns a future that |
| /// completes when the animation completes successfully, and completes with an |
| /// error when the animation is aborted. |
| /// |
| /// This can be used to write code such as: |
| /// |
| /// ```dart |
| /// Future<Null> fadeOutAndUpdateState() async { |
| /// try { |
| /// await fadeAnimationController.forward().orCancel; |
| /// await sizeAnimationController.forward().orCancel; |
| /// setState(() { |
| /// dismissed = true; |
| /// }); |
| /// } on TickerCanceled { |
| /// // the animation got canceled, probably because we were disposed |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// ...which asynchronously runs one animation, then runs another, then changes |
| /// the state of the widget, without having to verify [State.mounted] is still |
| /// true at each step, and without having to chain futures together explicitly. |
| /// (This assumes that the controllers are created in [State.initState] and |
| /// disposed in [State.dispose].) |
| 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.debugLabel, |
| this.lowerBound: 0.0, |
| this.upperBound: 1.0, |
| @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.debugLabel, |
| @required TickerProvider vsync, |
| }) : assert(value != null), |
| assert(vsync != null), |
| lowerBound = double.NEGATIVE_INFINITY, |
| 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; |
| |
| /// 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. |
| Duration duration; |
| |
| 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. |
| 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. |
| 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.MICROSECONDS_PER_SECOND); |
| } |
| |
| void _internalSetValue(double newValue) { |
| _value = newValue.clamp(lowerBound, upperBound); |
| 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 new FlutterError( |
| 'AnimationController.forward() called with no default Duration.\n' |
| 'The "duration" property should be set, either in the constructor or later, before ' |
| 'calling the forward() function.' |
| ); |
| } |
| return true; |
| }()); |
| _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) { |
| throw new FlutterError( |
| 'AnimationController.reverse() called with no default Duration.\n' |
| 'The "duration" property should be set, either in the constructor or later, before ' |
| 'calling the reverse() function.' |
| ); |
| } |
| return true; |
| }()); |
| _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 }) { |
| _direction = _AnimationDirection.forward; |
| return _animateToInternal(target, duration: duration, curve: curve); |
| } |
| |
| TickerFuture _animateToInternal(double target, { Duration duration, Curve curve: Curves.linear }) { |
| Duration simulationDuration = duration; |
| if (simulationDuration == null) { |
| assert(() { |
| if (this.duration == null) { |
| throw new FlutterError( |
| 'AnimationController.animateTo() called with no explicit Duration and no default Duration.\n' |
| 'Either the "duration" argument to the animateTo() method should be provided, or the ' |
| '"duration" property should be set, either in the constructor or later, before ' |
| 'calling the animateTo() function.' |
| ); |
| } |
| return true; |
| }()); |
| final double range = upperBound - lowerBound; |
| final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0; |
| simulationDuration = this.duration * 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); |
| notifyListeners(); |
| } |
| _status = (_direction == _AnimationDirection.forward) ? |
| AnimationStatus.completed : |
| AnimationStatus.dismissed; |
| _checkStatusChanged(); |
| return new TickerFuture.complete(); |
| } |
| assert(simulationDuration > Duration.ZERO); |
| assert(!isAnimating); |
| return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve)); |
| } |
| |
| /// Starts running this animation in the forward direction, and |
| /// restarts the animation when it completes. |
| /// |
| /// Defaults to repeating between the lower and upper bounds. |
| /// |
| /// 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, Duration period }) { |
| min ??= lowerBound; |
| max ??= upperBound; |
| period ??= duration; |
| assert(() { |
| if (period == null) { |
| throw new 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; |
| }()); |
| return animateWith(new _RepeatingSimulation(min, max, period)); |
| } |
| |
| /// 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 }) { |
| _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward; |
| final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance |
| : upperBound + _kFlingTolerance.distance; |
| final Simulation simulation = new SpringSimulation(_kFlingSpringDescription, value, target, velocity) |
| ..tolerance = _kFlingTolerance; |
| return animateWith(simulation); |
| } |
| |
| /// Drives the animation according to the given simulation. |
| /// |
| /// Returns a [TickerFuture] that completes when the animation is complete. |
| /// |
| /// The most recently returned [TickerFuture], if any, is marked as having been |
| /// canceled, meaning the future never completes and its [TickerFuture.orCancel] |
| /// derivative future completes with a [TickerCanceled] error. |
| TickerFuture animateWith(Simulation simulation) { |
| stop(); |
| 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); |
| final Future<Null> 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 `completed` argument with the value false, this is |
| /// reversed, and the futures complete successfully. |
| void stop({ bool canceled: true }) { |
| _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 new FlutterError( |
| 'AnimationController.dispose() called more than once.\n' |
| 'A given $runtimeType cannot be disposed more than once.\n' |
| 'The following $runtimeType object was disposed multiple times:\n' |
| ' $this' |
| ); |
| } |
| 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.MICROSECONDS_PER_SECOND; |
| assert(elapsedInSeconds >= 0.0); |
| _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound); |
| 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) |
| : assert(_begin != null), |
| assert(_end != null), |
| assert(duration != null && duration.inMicroseconds > 0), |
| _durationInSeconds = duration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND; |
| |
| 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); |
| 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; |
| } |
| |
| class _RepeatingSimulation extends Simulation { |
| _RepeatingSimulation(this.min, this.max, Duration period) |
| : _periodInSeconds = period.inMicroseconds / Duration.MICROSECONDS_PER_SECOND { |
| assert(_periodInSeconds > 0.0); |
| } |
| |
| final double min; |
| final double max; |
| |
| final double _periodInSeconds; |
| |
| @override |
| double x(double timeInSeconds) { |
| assert(timeInSeconds >= 0.0); |
| final double t = (timeInSeconds / _periodInSeconds) % 1.0; |
| return ui.lerpDouble(min, max, t); |
| } |
| |
| @override |
| double dx(double timeInSeconds) => (max - min) / _periodInSeconds; |
| |
| @override |
| bool isDone(double timeInSeconds) => false; |
| } |