animations.dart 23.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6
import 'dart:math' as math;
7

8 9
import 'package:flutter/foundation.dart';

10 11
import 'animation.dart';
import 'curves.dart';
12 13
import 'listener_helpers.dart';

14 15 16 17 18
export 'dart:ui' show VoidCallback;

export 'animation.dart' show Animation, AnimationStatus, AnimationStatusListener;
export 'curves.dart' show Curve;

19
// Examples can assume:
20
// late AnimationController controller;
21

22 23
class _AlwaysCompleteAnimation extends Animation<double> {
  const _AlwaysCompleteAnimation();
24

25
  @override
26
  void addListener(VoidCallback listener) { }
27 28

  @override
29
  void removeListener(VoidCallback listener) { }
30 31

  @override
32
  void addStatusListener(AnimationStatusListener listener) { }
33 34

  @override
35
  void removeStatusListener(AnimationStatusListener listener) { }
36 37

  @override
38
  AnimationStatus get status => AnimationStatus.completed;
39 40

  @override
41
  double get value => 1.0;
42 43 44

  @override
  String toString() => 'kAlwaysCompleteAnimation';
45 46
}

47 48 49 50 51
/// 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.
52
const Animation<double> kAlwaysCompleteAnimation = _AlwaysCompleteAnimation();
53

54 55
class _AlwaysDismissedAnimation extends Animation<double> {
  const _AlwaysDismissedAnimation();
56

57
  @override
58
  void addListener(VoidCallback listener) { }
59 60

  @override
61
  void removeListener(VoidCallback listener) { }
62 63

  @override
64
  void addStatusListener(AnimationStatusListener listener) { }
65 66

  @override
67
  void removeStatusListener(AnimationStatusListener listener) { }
68 69

  @override
70
  AnimationStatus get status => AnimationStatus.dismissed;
71 72

  @override
73
  double get value => 0.0;
74 75 76

  @override
  String toString() => 'kAlwaysDismissedAnimation';
77 78
}

79 80 81 82 83
/// 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.
84
const Animation<double> kAlwaysDismissedAnimation = _AlwaysDismissedAnimation();
85

86
/// An animation that is always stopped at a given value.
87 88
///
/// The [status] is always [AnimationStatus.forward].
89
class AlwaysStoppedAnimation<T> extends Animation<T> {
90 91 92
  /// Creates an [AlwaysStoppedAnimation] with the given value.
  ///
  /// Since the [value] and [status] of an [AlwaysStoppedAnimation] can never
93
  /// change, the listeners can never be called. It is therefore safe to reuse
94
  /// an [AlwaysStoppedAnimation] instance in multiple places. If the [value] to
95
  /// be used is known at compile time, the constructor should be called as a
96
  /// `const` constructor.
97 98
  const AlwaysStoppedAnimation(this.value);

99
  @override
100
  final T value;
101

102
  @override
103
  void addListener(VoidCallback listener) { }
104 105

  @override
106
  void removeListener(VoidCallback listener) { }
107 108

  @override
109
  void addStatusListener(AnimationStatusListener listener) { }
110 111

  @override
112
  void removeStatusListener(AnimationStatusListener listener) { }
113 114

  @override
115
  AnimationStatus get status => AnimationStatus.forward;
116 117 118 119 120

  @override
  String toStringDetails() {
    return '${super.toStringDetails()} $value; paused';
  }
121 122
}

123 124 125 126 127 128 129 130
/// 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.
131
mixin AnimationWithParentMixin<T> {
132 133 134
  /// The animation whose value this animation will proxy.
  ///
  /// This animation must remain the same for the lifetime of this object. If
135
  /// you wish to proxy a different animation at different times, consider using
136 137
  /// [ProxyAnimation].
  Animation<T> get parent;
138

139 140 141
  // keep these next five dartdocs in sync with the dartdocs in Animation<T>

  /// Calls the listener every time the value of the animation changes.
142 143
  ///
  /// Listeners can be removed with [removeListener].
144
  void addListener(VoidCallback listener) => parent.addListener(listener);
145 146

  /// Stop calling the listener every time the value of the animation changes.
147 148
  ///
  /// Listeners can be added with [addListener].
149
  void removeListener(VoidCallback listener) => parent.removeListener(listener);
150 151

  /// Calls listener every time the status of the animation changes.
152 153
  ///
  /// Listeners can be removed with [removeStatusListener].
154
  void addStatusListener(AnimationStatusListener listener) => parent.addStatusListener(listener);
155 156

  /// Stops calling the listener every time the status of the animation changes.
157 158
  ///
  /// Listeners can be added with [addStatusListener].
159 160
  void removeStatusListener(AnimationStatusListener listener) => parent.removeStatusListener(listener);

161
  /// The current status of this animation.
162 163 164
  AnimationStatus get status => parent.status;
}

165 166 167 168
/// 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
169
/// object, and then later change the animation from which the proxy receives
170
/// its value.
171
class ProxyAnimation extends Animation<double>
172
  with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
173 174 175 176 177

  /// 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.
178
  ProxyAnimation([Animation<double>? animation]) {
179 180
    _parent = animation;
    if (_parent == null) {
181
      _status = AnimationStatus.dismissed;
182 183 184 185
      _value = 0.0;
    }
  }

186 187
  AnimationStatus? _status;
  double? _value;
188

189 190 191 192
  /// 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.
193 194 195
  Animation<double>? get parent => _parent;
  Animation<double>? _parent;
  set parent(Animation<double>? value) {
196
    if (value == _parent) {
197
      return;
198
    }
199
    if (_parent != null) {
200 201
      _status = _parent!.status;
      _value = _parent!.value;
202
      if (isListening) {
203
        didStopListening();
204
      }
205
    }
206 207
    _parent = value;
    if (_parent != null) {
208
      if (isListening) {
209
        didStartListening();
210 211
      }
      if (_value != _parent!.value) {
212
        notifyListeners();
213 214
      }
      if (_status != _parent!.status) {
215
        notifyStatusListeners(_parent!.status);
216
      }
217 218 219 220 221
      _status = null;
      _value = null;
    }
  }

222
  @override
223
  void didStartListening() {
224
    if (_parent != null) {
225 226
      _parent!.addListener(notifyListeners);
      _parent!.addStatusListener(notifyStatusListeners);
227 228 229
    }
  }

230
  @override
231
  void didStopListening() {
232
    if (_parent != null) {
233 234
      _parent!.removeListener(notifyListeners);
      _parent!.removeStatusListener(notifyStatusListeners);
235 236 237
    }
  }

238
  @override
239
  AnimationStatus get status => _parent != null ? _parent!.status : _status!;
240 241

  @override
242
  double get value => _parent != null ? _parent!.value : _value!;
243 244 245

  @override
  String toString() {
246
    if (parent == null) {
247
      return '${objectRuntimeType(this, 'ProxyAnimation')}(null; ${super.toStringDetails()} ${value.toStringAsFixed(3)})';
248
    }
249
    return '$parent\u27A9${objectRuntimeType(this, 'ProxyAnimation')}';
250
  }
251 252
}

253 254 255
/// An animation that is the reverse of another animation.
///
/// If the parent animation is running forward from 0.0 to 1.0, this animation
256 257
/// is running in reverse from 1.0 to 0.0.
///
258 259
/// 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
260
/// or direction of the animation.
261 262 263 264 265 266 267
///
/// 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.
268
class ReverseAnimation extends Animation<double>
269
  with AnimationLazyListenerMixin, AnimationLocalStatusListenersMixin {
270 271 272 273

  /// Creates a reverse animation.
  ///
  /// The parent argument must not be null.
274
  ReverseAnimation(this.parent);
275

276
  /// The animation whose value and direction this animation is reversing.
277
  final Animation<double> parent;
278

279
  @override
280 281
  void addListener(VoidCallback listener) {
    didRegisterListener();
282
    parent.addListener(listener);
283
  }
284 285

  @override
286
  void removeListener(VoidCallback listener) {
287
    parent.removeListener(listener);
288 289 290
    didUnregisterListener();
  }

291
  @override
292
  void didStartListening() {
293
    parent.addStatusListener(_statusChangeHandler);
294 295
  }

296
  @override
297
  void didStopListening() {
298
    parent.removeStatusListener(_statusChangeHandler);
299 300
  }

301
  void _statusChangeHandler(AnimationStatus status) {
302 303 304
    notifyStatusListeners(_reverseStatus(status));
  }

305
  @override
306
  AnimationStatus get status => _reverseStatus(parent.status);
307 308

  @override
309
  double get value => 1.0 - parent.value;
310

311
  AnimationStatus _reverseStatus(AnimationStatus status) {
312
    switch (status) {
313 314 315 316
      case AnimationStatus.forward: return AnimationStatus.reverse;
      case AnimationStatus.reverse: return AnimationStatus.forward;
      case AnimationStatus.completed: return AnimationStatus.dismissed;
      case AnimationStatus.dismissed: return AnimationStatus.completed;
317 318
    }
  }
319 320 321

  @override
  String toString() {
322
    return '$parent\u27AA${objectRuntimeType(this, 'ReverseAnimation')}';
323
  }
324 325
}

326 327
/// An animation that applies a curve to another animation.
///
328
/// [CurvedAnimation] is useful when you want to apply a non-linear [Curve] to
329 330
/// an animation object, especially if you want different curves when the
/// animation is going forward vs when it is going backward.
331
///
332 333 334 335
/// 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.
336 337
///
/// If you want to apply a [Curve] to a [Tween], consider using [CurveTween].
338
///
339
/// {@tool snippet}
340 341 342 343 344 345 346 347 348 349
///
/// 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,
/// );
/// ```
350
/// {@end-tool}
351
/// {@tool snippet}
352 353 354 355 356 357 358 359 360 361 362 363 364
///
/// 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,
/// );
/// ```
365
/// {@end-tool}
366 367 368 369 370 371 372 373 374 375 376
///
/// 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].
377
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
378 379 380
  /// Creates a curved animation.
  ///
  /// The parent and curve arguments must not be null.
381
  CurvedAnimation({
382 383
    required this.parent,
    required this.curve,
384
    this.reverseCurve,
385
  }) {
386 387
    _updateCurveDirection(parent.status);
    parent.addStatusListener(_updateCurveDirection);
388 389
  }

390
  /// The animation to which this animation applies a curve.
391
  @override
392 393 394 395 396 397 398
  final Animation<double> parent;

  /// The curve to use in the forward direction.
  Curve curve;

  /// The curve to use in the reverse direction.
  ///
399 400 401 402 403 404 405 406 407 408
  /// 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.
  ///
409
  /// If this field is null, uses [curve] in both directions.
410
  Curve? reverseCurve;
411 412 413 414 415

  /// 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
416
  /// animation is used to animate.
417
  AnimationStatus? _curveDirection;
418

419 420 421
  /// True if this CurvedAnimation has been disposed.
  bool isDisposed = false;

422
  void _updateCurveDirection(AnimationStatus status) {
423 424 425 426 427
    switch (status) {
      case AnimationStatus.dismissed:
      case AnimationStatus.completed:
        _curveDirection = null;
      case AnimationStatus.forward:
Adam Barth's avatar
Adam Barth committed
428
        _curveDirection ??= AnimationStatus.forward;
429
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
430
        _curveDirection ??= AnimationStatus.reverse;
431 432 433
    }
  }

434 435 436 437
  bool get _useForwardCurve {
    return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse;
  }

438 439 440 441 442 443
  /// Cleans up any listeners added by this CurvedAnimation.
  void dispose() {
    isDisposed = true;
    parent.removeStatusListener(_updateCurveDirection);
  }

444
  @override
445
  double get value {
446
    final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;
447

448
    final double t = parent.value;
449
    if (activeCurve == null) {
450
      return t;
451
    }
452
    if (t == 0.0 || t == 1.0) {
453 454 455 456
      assert(() {
        final double transformedValue = activeCurve.transform(t);
        final double roundedTransformedValue = transformedValue.round().toDouble();
        if (roundedTransformedValue != t) {
457 458 459 460
          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 '
461
            'is near $roundedTransformedValue.',
462
          );
463 464
        }
        return true;
465
      }());
466 467 468 469
      return t;
    }
    return activeCurve.transform(t);
  }
470 471 472

  @override
  String toString() {
473
    if (reverseCurve == null) {
474
      return '$parent\u27A9$curve';
475 476
    }
    if (_useForwardCurve) {
477
      return '$parent\u27A9$curve\u2092\u2099/$reverseCurve';
478
    }
479 480
    return '$parent\u27A9$curve/$reverseCurve\u2092\u2099';
  }
481 482
}

483 484
enum _TrainHoppingMode { minimize, maximize }

485 486
/// 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
487
/// going in the opposite direction, or because the one overtakes the other),
488 489 490 491 492 493 494 495 496 497
/// 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.
498 499 500 501 502
///
/// 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.
503
class TrainHoppingAnimation extends Animation<double>
504
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
505 506 507 508

  /// Creates a train-hopping animation.
  ///
  /// The current train argument must not be null but the next train argument
509 510
  /// can be null. If the next train is null, then this object will just proxy
  /// the first animation and never hop.
511 512 513 514
  TrainHoppingAnimation(
    Animation<double> this._currentTrain,
    this._nextTrain, {
    this.onSwitchedTrain,
515
  }) {
516
    if (_nextTrain != null) {
517
      if (_currentTrain!.value == _nextTrain!.value) {
518
        _currentTrain = _nextTrain;
519
        _nextTrain = null;
520
      } else if (_currentTrain!.value > _nextTrain!.value) {
521 522
        _mode = _TrainHoppingMode.maximize;
      } else {
523
        assert(_currentTrain!.value < _nextTrain!.value);
524 525 526
        _mode = _TrainHoppingMode.minimize;
      }
    }
527 528
    _currentTrain!.addStatusListener(_statusChangeHandler);
    _currentTrain!.addListener(_valueChangeHandler);
529
    _nextTrain?.addListener(_valueChangeHandler);
530
    assert(_mode != null || _nextTrain == null);
531 532
  }

533 534 535 536
  /// 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.
537 538 539 540
  Animation<double>? get currentTrain => _currentTrain;
  Animation<double>? _currentTrain;
  Animation<double>? _nextTrain;
  _TrainHoppingMode? _mode;
541

542 543 544 545 546
  /// 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.
547
  VoidCallback? onSwitchedTrain;
548

549
  AnimationStatus? _lastStatus;
550
  void _statusChangeHandler(AnimationStatus status) {
551 552 553 554 555 556 557 558
    assert(_currentTrain != null);
    if (status != _lastStatus) {
      notifyListeners();
      _lastStatus = status;
    }
    assert(_lastStatus != null);
  }

559
  @override
560
  AnimationStatus get status => _currentTrain!.status;
561

562
  double? _lastValue;
563 564 565 566
  void _valueChangeHandler() {
    assert(_currentTrain != null);
    bool hop = false;
    if (_nextTrain != null) {
567
      assert(_mode != null);
568
      switch (_mode!) {
569
        case _TrainHoppingMode.minimize:
570
          hop = _nextTrain!.value <= _currentTrain!.value;
571
        case _TrainHoppingMode.maximize:
572
          hop = _nextTrain!.value >= _currentTrain!.value;
573 574
      }
      if (hop) {
575
        _currentTrain!
576 577
          ..removeStatusListener(_statusChangeHandler)
          ..removeListener(_valueChangeHandler);
578
        _currentTrain = _nextTrain;
579
        _nextTrain = null;
580 581
        _currentTrain!.addStatusListener(_statusChangeHandler);
        _statusChangeHandler(_currentTrain!.status);
582 583
      }
    }
584
    final double newValue = value;
585 586 587 588 589
    if (newValue != _lastValue) {
      notifyListeners();
      _lastValue = newValue;
    }
    assert(_lastValue != null);
590
    if (hop && onSwitchedTrain != null) {
591
      onSwitchedTrain!();
592
    }
593 594
  }

595
  @override
596
  double get value => _currentTrain!.value;
597 598 599

  /// Frees all the resources used by this performance.
  /// After this is called, this object is no longer usable.
600
  @override
601 602
  void dispose() {
    assert(_currentTrain != null);
603 604
    _currentTrain!.removeStatusListener(_statusChangeHandler);
    _currentTrain!.removeListener(_valueChangeHandler);
605
    _currentTrain = null;
606 607
    _nextTrain?.removeListener(_valueChangeHandler);
    _nextTrain = null;
608 609
    clearListeners();
    clearStatusListeners();
610
    super.dispose();
611
  }
612 613 614

  @override
  String toString() {
615
    if (_nextTrain != null) {
616
      return '$currentTrain\u27A9${objectRuntimeType(this, 'TrainHoppingAnimation')}(next: $_nextTrain)';
617
    }
618
    return '$currentTrain\u27A9${objectRuntimeType(this, 'TrainHoppingAnimation')}(no next)';
619
  }
620
}
621 622 623 624 625 626 627

/// 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;`
628 629 630
///
/// By default, the [status] of a [CompoundAnimation] is the status of the
/// [next] animation if [next] is moving, and the status of the [first]
631
/// animation otherwise.
632 633 634 635 636
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({
637 638
    required this.first,
    required this.next,
639
  });
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663

  /// 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);
  }

664
  /// Gets the status of this animation based on the [first] and [next] status.
665
  ///
666 667
  /// The default is that if the [next] animation is moving, use its status.
  /// Otherwise, default to [first].
668 669
  @override
  AnimationStatus get status {
670
    if (next.status == AnimationStatus.forward || next.status == AnimationStatus.reverse) {
671
      return next.status;
672
    }
673 674 675 676 677
    return first.status;
  }

  @override
  String toString() {
678
    return '${objectRuntimeType(this, 'CompoundAnimation')}($first, $next)';
679 680
  }

681
  AnimationStatus? _lastStatus;
682
  void _maybeNotifyStatusListeners(AnimationStatus _) {
683 684 685
    if (status != _lastStatus) {
      _lastStatus = status;
      notifyStatusListeners(status);
686 687 688
    }
  }

689
  T? _lastValue;
690
  void _maybeNotifyListeners() {
691 692
    if (value != _lastValue) {
      _lastValue = value;
693 694 695 696
      notifyListeners();
    }
  }
}
697 698 699 700 701 702 703 704 705 706 707

/// 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({
708 709
    required Animation<double> left,
    required Animation<double> right,
710 711 712 713 714
  }) : super(first: left, next: right);

  @override
  double get value => (first.value + next.value) / 2.0;
}
715 716 717

/// An animation that tracks the maximum of two other animations.
///
718
/// The [value] of this animation is the maximum of the values of
719 720
/// [first] and [next].
class AnimationMax<T extends num> extends CompoundAnimation<T> {
721 722 723 724
  /// Creates an [AnimationMax].
  ///
  /// Both arguments must be non-null. Either can be an [AnimationMax] itself
  /// to combine multiple animations.
725
  AnimationMax(Animation<T> first, Animation<T> next) : super(first: first, next: next);
726 727 728 729 730 731 732

  @override
  T get value => math.max(first.value, next.value);
}

/// An animation that tracks the minimum of two other animations.
///
733
/// The [value] of this animation is the maximum of the values of
734 735
/// [first] and [next].
class AnimationMin<T extends num> extends CompoundAnimation<T> {
736 737 738 739
  /// Creates an [AnimationMin].
  ///
  /// Both arguments must be non-null. Either can be an [AnimationMin] itself
  /// to combine multiple animations.
740
  AnimationMin(Animation<T> first, Animation<T> next) : super(first: first, next: next);
741 742 743

  @override
  T get value => math.min(first.value, next.value);
744
}