blob: 32ab062ff1beba873c23269baa4ed24216fca2e6 [file] [log] [blame]
// 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;
}