| // 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'; |
| |
| // Examples can assume: |
| // 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 simply 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) |
| : assert(parent != null); |
| |
| /// 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) { |
| assert(status != null); |
| 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; |
| } |
| return null; |
| } |
| |
| @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, |
| }) : assert(parent != null), |
| assert(curve != null) { |
| _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; |
| |
| void _updateCurveDirection(AnimationStatus status) { |
| switch (status) { |
| case AnimationStatus.dismissed: |
| case AnimationStatus.completed: |
| _curveDirection = null; |
| break; |
| case AnimationStatus.forward: |
| _curveDirection ??= AnimationStatus.forward; |
| break; |
| case AnimationStatus.reverse: |
| _curveDirection ??= AnimationStatus.reverse; |
| break; |
| } |
| } |
| |
| bool get _useForwardCurve { |
| return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse; |
| } |
| |
| @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(this._currentTrain, this._nextTrain, { this.onSwitchedTrain }) |
| : assert(_currentTrain != null) { |
| 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; |
| break; |
| case _TrainHoppingMode.maximize: |
| hop = _nextTrain.value >= _currentTrain.value; |
| break; |
| } |
| 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; |
| 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, |
| }) : assert(first != null), |
| assert(next != null); |
| |
| /// 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({ |
| Animation<double> left, |
| 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); |
| } |