// 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:math' as math; import 'package:flutter/foundation.dart'; import 'animation.dart'; import 'curves.dart'; import 'listener_helpers.dart'; export 'dart:ui' show VoidCallback; export 'animation.dart' show Animation, AnimationStatus, AnimationStatusListener; export 'curves.dart' show Curve; // Examples can assume: // late AnimationController controller; class _AlwaysCompleteAnimation extends Animation<double> { const _AlwaysCompleteAnimation(); @override void addListener(VoidCallback listener) { } @override void removeListener(VoidCallback listener) { } @override void addStatusListener(AnimationStatusListener listener) { } @override void removeStatusListener(AnimationStatusListener listener) { } @override AnimationStatus get status => AnimationStatus.completed; @override double get value => 1.0; @override String toString() => 'kAlwaysCompleteAnimation'; } /// An animation that is always complete. /// /// Using this constant involves less overhead than building an /// [AnimationController] with an initial value of 1.0. This is useful when an /// API expects an animation but you don't actually want to animate anything. const Animation<double> kAlwaysCompleteAnimation = _AlwaysCompleteAnimation(); class _AlwaysDismissedAnimation extends Animation<double> { const _AlwaysDismissedAnimation(); @override void addListener(VoidCallback listener) { } @override void removeListener(VoidCallback listener) { } @override void addStatusListener(AnimationStatusListener listener) { } @override void removeStatusListener(AnimationStatusListener listener) { } @override AnimationStatus get status => AnimationStatus.dismissed; @override double get value => 0.0; @override String toString() => 'kAlwaysDismissedAnimation'; } /// An animation that is always dismissed. /// /// Using this constant involves less overhead than building an /// [AnimationController] with an initial value of 0.0. This is useful when an /// API expects an animation but you don't actually want to animate anything. const Animation<double> kAlwaysDismissedAnimation = _AlwaysDismissedAnimation(); /// An animation that is always stopped at a given value. /// /// The [status] is always [AnimationStatus.forward]. class AlwaysStoppedAnimation<T> extends Animation<T> { /// Creates an [AlwaysStoppedAnimation] with the given value. /// /// Since the [value] and [status] of an [AlwaysStoppedAnimation] can never /// change, the listeners can never be called. It is therefore safe to reuse /// an [AlwaysStoppedAnimation] instance in multiple places. If the [value] to /// be used is known at compile time, the constructor should be called as a /// `const` constructor. const AlwaysStoppedAnimation(this.value); @override final T value; @override void addListener(VoidCallback listener) { } @override void removeListener(VoidCallback listener) { } @override void addStatusListener(AnimationStatusListener listener) { } @override void removeStatusListener(AnimationStatusListener listener) { } @override AnimationStatus get status => AnimationStatus.forward; @override String toStringDetails() { return '${super.toStringDetails()} $value; paused'; } } /// Implements most of the [Animation] interface by deferring its behavior to a /// given [parent] Animation. /// /// To implement an [Animation] that is driven by a parent, it is only necessary /// to mix in this class, implement [parent], and implement `T get value`. /// /// To define a mapping from values in the range 0..1, consider subclassing /// [Tween] instead. mixin AnimationWithParentMixin<T> { /// The animation whose value this animation will proxy. /// /// This animation must remain the same for the lifetime of this object. If /// you wish to proxy a different animation at different times, consider using /// [ProxyAnimation]. Animation<T> get parent; // keep these next five dartdocs in sync with the dartdocs in Animation<T> /// Calls the listener every time the value of the animation changes. /// /// Listeners can be removed with [removeListener]. void addListener(VoidCallback listener) => parent.addListener(listener); /// Stop calling the listener every time the value of the animation changes. /// /// Listeners can be added with [addListener]. void removeListener(VoidCallback listener) => parent.removeListener(listener); /// Calls listener every time the status of the animation changes. /// /// Listeners can be removed with [removeStatusListener]. void addStatusListener(AnimationStatusListener listener) => parent.addStatusListener(listener); /// Stops calling the listener every time the status of the animation changes. /// /// Listeners can be added with [addStatusListener]. void removeStatusListener(AnimationStatusListener listener) => parent.removeStatusListener(listener); /// The current status of this animation. AnimationStatus get status => parent.status; } /// An animation that is a proxy for another animation. /// /// A proxy animation is useful because the parent animation can be mutated. For /// example, one object can create a proxy animation, hand the proxy to another /// object, and then later change the animation from which the proxy receives /// its value. class ProxyAnimation extends Animation<double> with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { /// Creates a proxy animation. /// /// If the animation argument is omitted, the proxy animation will have the /// status [AnimationStatus.dismissed] and a value of 0.0. ProxyAnimation([Animation<double>? animation]) { _parent = animation; if (_parent == null) { _status = AnimationStatus.dismissed; _value = 0.0; } } AnimationStatus? _status; double? _value; /// The animation whose value this animation will proxy. /// /// This value is mutable. When mutated, the listeners on the proxy animation /// will be transparently updated to be listening to the new parent animation. Animation<double>? get parent => _parent; Animation<double>? _parent; set parent(Animation<double>? value) { if (value == _parent) { return; } if (_parent != null) { _status = _parent!.status; _value = _parent!.value; if (isListening) { didStopListening(); } } _parent = value; if (_parent != null) { if (isListening) { didStartListening(); } if (_value != _parent!.value) { notifyListeners(); } if (_status != _parent!.status) { notifyStatusListeners(_parent!.status); } _status = null; _value = null; } } @override void didStartListening() { if (_parent != null) { _parent!.addListener(notifyListeners); _parent!.addStatusListener(notifyStatusListeners); } } @override void didStopListening() { if (_parent != null) { _parent!.removeListener(notifyListeners); _parent!.removeStatusListener(notifyStatusListeners); } } @override AnimationStatus get status => _parent != null ? _parent!.status : _status!; @override double get value => _parent != null ? _parent!.value : _value!; @override String toString() { if (parent == null) { return '${objectRuntimeType(this, 'ProxyAnimation')}(null; ${super.toStringDetails()} ${value.toStringAsFixed(3)})'; } return '$parent\u27A9${objectRuntimeType(this, 'ProxyAnimation')}'; } } /// An animation that is the reverse of another animation. /// /// If the parent animation is running forward from 0.0 to 1.0, this animation /// is running in reverse from 1.0 to 0.0. /// /// Using a [ReverseAnimation] is different from using a [Tween] with a /// `begin` of 1.0 and an `end` of 0.0 because the tween does not change the status /// or direction of the animation. /// /// See also: /// /// * [Curve.flipped] and [FlippedCurve], which provide a similar effect but on /// [Curve]s. /// * [CurvedAnimation], which can take separate curves for when the animation /// is going forward than for when it is going in reverse. class ReverseAnimation extends Animation<double> with AnimationLazyListenerMixin, AnimationLocalStatusListenersMixin { /// Creates a reverse animation. /// /// The parent argument must not be null. ReverseAnimation(this.parent); /// The animation whose value and direction this animation is reversing. final Animation<double> parent; @override void addListener(VoidCallback listener) { didRegisterListener(); parent.addListener(listener); } @override void removeListener(VoidCallback listener) { parent.removeListener(listener); didUnregisterListener(); } @override void didStartListening() { parent.addStatusListener(_statusChangeHandler); } @override void didStopListening() { parent.removeStatusListener(_statusChangeHandler); } void _statusChangeHandler(AnimationStatus status) { notifyStatusListeners(_reverseStatus(status)); } @override AnimationStatus get status => _reverseStatus(parent.status); @override double get value => 1.0 - parent.value; AnimationStatus _reverseStatus(AnimationStatus status) { switch (status) { case AnimationStatus.forward: return AnimationStatus.reverse; case AnimationStatus.reverse: return AnimationStatus.forward; case AnimationStatus.completed: return AnimationStatus.dismissed; case AnimationStatus.dismissed: return AnimationStatus.completed; } } @override String toString() { return '$parent\u27AA${objectRuntimeType(this, 'ReverseAnimation')}'; } } /// An animation that applies a curve to another animation. /// /// [CurvedAnimation] is useful when you want to apply a non-linear [Curve] to /// an animation object, especially if you want different curves when the /// animation is going forward vs when it is going backward. /// /// Depending on the given curve, the output of the [CurvedAnimation] could have /// a wider range than its input. For example, elastic curves such as /// [Curves.elasticIn] will significantly overshoot or undershoot the default /// range of 0.0 to 1.0. /// /// If you want to apply a [Curve] to a [Tween], consider using [CurveTween]. /// /// {@tool snippet} /// /// The following code snippet shows how you can apply a curve to a linear /// animation produced by an [AnimationController] `controller`. /// /// ```dart /// final Animation<double> animation = CurvedAnimation( /// parent: controller, /// curve: Curves.ease, /// ); /// ``` /// {@end-tool} /// {@tool snippet} /// /// This second code snippet shows how to apply a different curve in the forward /// direction than in the reverse direction. This can't be done using a /// [CurveTween] (since [Tween]s are not aware of the animation direction when /// they are applied). /// /// ```dart /// final Animation<double> animation = CurvedAnimation( /// parent: controller, /// curve: Curves.easeIn, /// reverseCurve: Curves.easeOut, /// ); /// ``` /// {@end-tool} /// /// By default, the [reverseCurve] matches the forward [curve]. /// /// See also: /// /// * [CurveTween], for an alternative way of expressing the first sample /// above. /// * [AnimationController], for examples of creating and disposing of an /// [AnimationController]. /// * [Curve.flipped] and [FlippedCurve], which provide the reverse of a /// [Curve]. class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> { /// Creates a curved animation. /// /// The parent and curve arguments must not be null. CurvedAnimation({ required this.parent, required this.curve, this.reverseCurve, }) { _updateCurveDirection(parent.status); parent.addStatusListener(_updateCurveDirection); } /// The animation to which this animation applies a curve. @override final Animation<double> parent; /// The curve to use in the forward direction. Curve curve; /// The curve to use in the reverse direction. /// /// If the parent animation changes direction without first reaching the /// [AnimationStatus.completed] or [AnimationStatus.dismissed] status, the /// [CurvedAnimation] stays on the same curve (albeit in the opposite /// direction) to avoid visual discontinuities. /// /// If you use a non-null [reverseCurve], you might want to hold this object /// in a [State] object rather than recreating it each time your widget builds /// in order to take advantage of the state in this object that avoids visual /// discontinuities. /// /// If this field is null, uses [curve] in both directions. Curve? reverseCurve; /// The direction used to select the current curve. /// /// The curve direction is only reset when we hit the beginning or the end of /// the timeline to avoid discontinuities in the value of any variables this /// animation is used to animate. AnimationStatus? _curveDirection; /// True if this CurvedAnimation has been disposed. bool isDisposed = false; void _updateCurveDirection(AnimationStatus status) { switch (status) { case AnimationStatus.dismissed: case AnimationStatus.completed: _curveDirection = null; case AnimationStatus.forward: _curveDirection ??= AnimationStatus.forward; case AnimationStatus.reverse: _curveDirection ??= AnimationStatus.reverse; } } bool get _useForwardCurve { return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse; } /// Cleans up any listeners added by this CurvedAnimation. void dispose() { isDisposed = true; parent.removeStatusListener(_updateCurveDirection); } @override double get value { final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve; final double t = parent.value; if (activeCurve == null) { return t; } if (t == 0.0 || t == 1.0) { assert(() { final double transformedValue = activeCurve.transform(t); final double roundedTransformedValue = transformedValue.round().toDouble(); if (roundedTransformedValue != t) { throw FlutterError( 'Invalid curve endpoint at $t.\n' 'Curves must map 0.0 to near zero and 1.0 to near one but ' '${activeCurve.runtimeType} mapped $t to $transformedValue, which ' 'is near $roundedTransformedValue.', ); } return true; }()); return t; } return activeCurve.transform(t); } @override String toString() { if (reverseCurve == null) { return '$parent\u27A9$curve'; } if (_useForwardCurve) { return '$parent\u27A9$curve\u2092\u2099/$reverseCurve'; } return '$parent\u27A9$curve/$reverseCurve\u2092\u2099'; } } enum _TrainHoppingMode { minimize, maximize } /// This animation starts by proxying one animation, but when the value of that /// animation crosses the value of the second (either because the second is /// going in the opposite direction, or because the one overtakes the other), /// the animation hops over to proxying the second animation. /// /// When the [TrainHoppingAnimation] starts proxying the second animation /// instead of the first, the [onSwitchedTrain] callback is called. /// /// If the two animations start at the same value, then the /// [TrainHoppingAnimation] immediately hops to the second animation, and the /// [onSwitchedTrain] callback is not called. If only one animation is provided /// (i.e. if the second is null), then the [TrainHoppingAnimation] just proxies /// the first animation. /// /// Since this object must track the two animations even when it has no /// listeners of its own, instead of shutting down when all its listeners are /// removed, it exposes a [dispose()] method. Call this method to shut this /// object down. class TrainHoppingAnimation extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { /// Creates a train-hopping animation. /// /// The current train argument must not be null but the next train argument /// can be null. If the next train is null, then this object will just proxy /// the first animation and never hop. TrainHoppingAnimation( Animation<double> this._currentTrain, this._nextTrain, { this.onSwitchedTrain, }) { if (_nextTrain != null) { if (_currentTrain!.value == _nextTrain!.value) { _currentTrain = _nextTrain; _nextTrain = null; } else if (_currentTrain!.value > _nextTrain!.value) { _mode = _TrainHoppingMode.maximize; } else { assert(_currentTrain!.value < _nextTrain!.value); _mode = _TrainHoppingMode.minimize; } } _currentTrain!.addStatusListener(_statusChangeHandler); _currentTrain!.addListener(_valueChangeHandler); _nextTrain?.addListener(_valueChangeHandler); assert(_mode != null || _nextTrain == null); } /// The animation that is currently driving this animation. /// /// The identity of this object will change from the first animation to the /// second animation when [onSwitchedTrain] is called. Animation<double>? get currentTrain => _currentTrain; Animation<double>? _currentTrain; Animation<double>? _nextTrain; _TrainHoppingMode? _mode; /// Called when this animation switches to be driven by the second animation. /// /// This is not called if the two animations provided to the constructor have /// the same value at the time of the call to the constructor. In that case, /// the second animation is used from the start, and the first is ignored. VoidCallback? onSwitchedTrain; AnimationStatus? _lastStatus; void _statusChangeHandler(AnimationStatus status) { assert(_currentTrain != null); if (status != _lastStatus) { notifyListeners(); _lastStatus = status; } assert(_lastStatus != null); } @override AnimationStatus get status => _currentTrain!.status; double? _lastValue; void _valueChangeHandler() { assert(_currentTrain != null); bool hop = false; if (_nextTrain != null) { assert(_mode != null); switch (_mode!) { case _TrainHoppingMode.minimize: hop = _nextTrain!.value <= _currentTrain!.value; case _TrainHoppingMode.maximize: hop = _nextTrain!.value >= _currentTrain!.value; } if (hop) { _currentTrain! ..removeStatusListener(_statusChangeHandler) ..removeListener(_valueChangeHandler); _currentTrain = _nextTrain; _nextTrain = null; _currentTrain!.addStatusListener(_statusChangeHandler); _statusChangeHandler(_currentTrain!.status); } } final double newValue = value; if (newValue != _lastValue) { notifyListeners(); _lastValue = newValue; } assert(_lastValue != null); if (hop && onSwitchedTrain != null) { onSwitchedTrain!(); } } @override double get value => _currentTrain!.value; /// Frees all the resources used by this performance. /// After this is called, this object is no longer usable. @override void dispose() { assert(_currentTrain != null); _currentTrain!.removeStatusListener(_statusChangeHandler); _currentTrain!.removeListener(_valueChangeHandler); _currentTrain = null; _nextTrain?.removeListener(_valueChangeHandler); _nextTrain = null; clearListeners(); clearStatusListeners(); super.dispose(); } @override String toString() { if (_nextTrain != null) { return '$currentTrain\u27A9${objectRuntimeType(this, 'TrainHoppingAnimation')}(next: $_nextTrain)'; } return '$currentTrain\u27A9${objectRuntimeType(this, 'TrainHoppingAnimation')}(no next)'; } } /// An interface for combining multiple Animations. Subclasses need only /// implement the `value` getter to control how the child animations are /// combined. Can be chained to combine more than 2 animations. /// /// For example, to create an animation that is the sum of two others, subclass /// this class and define `T get value = first.value + second.value;` /// /// By default, the [status] of a [CompoundAnimation] is the status of the /// [next] animation if [next] is moving, and the status of the [first] /// animation otherwise. abstract class CompoundAnimation<T> extends Animation<T> with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { /// Creates a CompoundAnimation. Both arguments must be non-null. Either can /// be a CompoundAnimation itself to combine multiple animations. CompoundAnimation({ required this.first, required this.next, }); /// The first sub-animation. Its status takes precedence if neither are /// animating. final Animation<T> first; /// The second sub-animation. final Animation<T> next; @override void didStartListening() { first.addListener(_maybeNotifyListeners); first.addStatusListener(_maybeNotifyStatusListeners); next.addListener(_maybeNotifyListeners); next.addStatusListener(_maybeNotifyStatusListeners); } @override void didStopListening() { first.removeListener(_maybeNotifyListeners); first.removeStatusListener(_maybeNotifyStatusListeners); next.removeListener(_maybeNotifyListeners); next.removeStatusListener(_maybeNotifyStatusListeners); } /// Gets the status of this animation based on the [first] and [next] status. /// /// The default is that if the [next] animation is moving, use its status. /// Otherwise, default to [first]. @override AnimationStatus get status { if (next.status == AnimationStatus.forward || next.status == AnimationStatus.reverse) { return next.status; } return first.status; } @override String toString() { return '${objectRuntimeType(this, 'CompoundAnimation')}($first, $next)'; } AnimationStatus? _lastStatus; void _maybeNotifyStatusListeners(AnimationStatus _) { if (status != _lastStatus) { _lastStatus = status; notifyStatusListeners(status); } } T? _lastValue; void _maybeNotifyListeners() { if (value != _lastValue) { _lastValue = value; notifyListeners(); } } } /// An animation of [double]s that tracks the mean of two other animations. /// /// The [status] of this animation is the status of the `right` animation if it is /// moving, and the `left` animation otherwise. /// /// The [value] of this animation is the [double] that represents the mean value /// of the values of the `left` and `right` animations. class AnimationMean extends CompoundAnimation<double> { /// Creates an animation that tracks the mean of two other animations. AnimationMean({ required Animation<double> left, required Animation<double> right, }) : super(first: left, next: right); @override double get value => (first.value + next.value) / 2.0; } /// An animation that tracks the maximum of two other animations. /// /// The [value] of this animation is the maximum of the values of /// [first] and [next]. class AnimationMax<T extends num> extends CompoundAnimation<T> { /// Creates an [AnimationMax]. /// /// Both arguments must be non-null. Either can be an [AnimationMax] itself /// to combine multiple animations. AnimationMax(Animation<T> first, Animation<T> next) : super(first: first, next: next); @override T get value => math.max(first.value, next.value); } /// An animation that tracks the minimum of two other animations. /// /// The [value] of this animation is the maximum of the values of /// [first] and [next]. class AnimationMin<T extends num> extends CompoundAnimation<T> { /// Creates an [AnimationMin]. /// /// Both arguments must be non-null. Either can be an [AnimationMin] itself /// to combine multiple animations. AnimationMin(Animation<T> first, Animation<T> next) : super(first: first, next: next); @override T get value => math.min(first.value, next.value); }