animations.dart 19.7 KB
Newer Older
1 2 3 4 5 6
// 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:ui' show VoidCallback;

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

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

13 14
class _AlwaysCompleteAnimation extends Animation<double> {
  const _AlwaysCompleteAnimation();
15

16
  @override
17
  void addListener(VoidCallback listener) { }
18 19

  @override
20
  void removeListener(VoidCallback listener) { }
21 22

  @override
23
  void addStatusListener(AnimationStatusListener listener) { }
24 25

  @override
26
  void removeStatusListener(AnimationStatusListener listener) { }
27 28

  @override
29
  AnimationStatus get status => AnimationStatus.completed;
30 31

  @override
32
  double get value => 1.0;
33 34 35

  @override
  String toString() => 'kAlwaysCompleteAnimation';
36 37
}

38 39 40 41 42 43
/// 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 = const _AlwaysCompleteAnimation();
44

45 46
class _AlwaysDismissedAnimation extends Animation<double> {
  const _AlwaysDismissedAnimation();
47

48
  @override
49
  void addListener(VoidCallback listener) { }
50 51

  @override
52
  void removeListener(VoidCallback listener) { }
53 54

  @override
55
  void addStatusListener(AnimationStatusListener listener) { }
56 57

  @override
58
  void removeStatusListener(AnimationStatusListener listener) { }
59 60

  @override
61
  AnimationStatus get status => AnimationStatus.dismissed;
62 63

  @override
64
  double get value => 0.0;
65 66 67

  @override
  String toString() => 'kAlwaysDismissedAnimation';
68 69
}

70 71 72 73 74 75
/// 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 = const _AlwaysDismissedAnimation();
76

77
/// An animation that is always stopped at a given value.
78 79
///
/// The [status] is always [AnimationStatus.forward].
80
class AlwaysStoppedAnimation<T> extends Animation<T> {
81 82 83
  /// Creates an [AlwaysStoppedAnimation] with the given value.
  ///
  /// Since the [value] and [status] of an [AlwaysStoppedAnimation] can never
84
  /// change, the listeners can never be called. It is therefore safe to reuse
85
  /// an [AlwaysStoppedAnimation] instance in multiple places. If the [value] to
86
  /// be used is known at compile time, the constructor should be called as a
87
  /// `const` constructor.
88 89
  const AlwaysStoppedAnimation(this.value);

90
  @override
91
  final T value;
92

93
  @override
94
  void addListener(VoidCallback listener) { }
95 96

  @override
97
  void removeListener(VoidCallback listener) { }
98 99

  @override
100
  void addStatusListener(AnimationStatusListener listener) { }
101 102

  @override
103
  void removeStatusListener(AnimationStatusListener listener) { }
104 105

  @override
106
  AnimationStatus get status => AnimationStatus.forward;
107 108 109 110 111

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

114 115 116
/// Implements most of the [Animation] interface, by deferring its behavior to a
/// given [parent] Animation. To implement an [Animation] that proxies to a
/// parent, this class plus implementing "T get value" is all that is necessary.
117
abstract class AnimationWithParentMixin<T> {
118 119 120
  /// The animation whose value this animation will proxy.
  ///
  /// This animation must remain the same for the lifetime of this object. If
121
  /// you wish to proxy a different animation at different times, consider using
122 123
  /// [ProxyAnimation].
  Animation<T> get parent;
124

125 126 127
  // keep these next five dartdocs in sync with the dartdocs in Animation<T>

  /// Calls the listener every time the value of the animation changes.
128 129
  ///
  /// Listeners can be removed with [removeListener].
130
  void addListener(VoidCallback listener) => parent.addListener(listener);
131 132

  /// Stop calling the listener every time the value of the animation changes.
133 134
  ///
  /// Listeners can be added with [addListener].
135
  void removeListener(VoidCallback listener) => parent.removeListener(listener);
136 137

  /// Calls listener every time the status of the animation changes.
138 139
  ///
  /// Listeners can be removed with [removeStatusListener].
140
  void addStatusListener(AnimationStatusListener listener) => parent.addStatusListener(listener);
141 142

  /// Stops calling the listener every time the status of the animation changes.
143 144
  ///
  /// Listeners can be added with [addStatusListener].
145 146
  void removeStatusListener(AnimationStatusListener listener) => parent.removeStatusListener(listener);

147
  /// The current status of this animation.
148 149 150
  AnimationStatus get status => parent.status;
}

151 152 153 154 155 156
/// 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 receieves
/// its value.
157
class ProxyAnimation extends Animation<double>
158
  with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
159 160 161 162 163

  /// 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.
164
  ProxyAnimation([Animation<double> animation]) {
165 166
    _parent = animation;
    if (_parent == null) {
167
      _status = AnimationStatus.dismissed;
168 169 170 171
      _value = 0.0;
    }
  }

172
  AnimationStatus _status;
173 174
  double _value;

175 176 177 178
  /// 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.
179 180
  Animation<double> get parent => _parent;
  Animation<double> _parent;
181
  set parent(Animation<double> value) {
182
    if (value == _parent)
183
      return;
184 185 186
    if (_parent != null) {
      _status = _parent.status;
      _value = _parent.value;
187 188 189
      if (isListening)
        didStopListening();
    }
190 191
    _parent = value;
    if (_parent != null) {
192 193
      if (isListening)
        didStartListening();
194
      if (_value != _parent.value)
195
        notifyListeners();
196 197
      if (_status != _parent.status)
        notifyStatusListeners(_parent.status);
198 199 200 201 202
      _status = null;
      _value = null;
    }
  }

203
  @override
204
  void didStartListening() {
205 206 207
    if (_parent != null) {
      _parent.addListener(notifyListeners);
      _parent.addStatusListener(notifyStatusListeners);
208 209 210
    }
  }

211
  @override
212
  void didStopListening() {
213 214 215
    if (_parent != null) {
      _parent.removeListener(notifyListeners);
      _parent.removeStatusListener(notifyStatusListeners);
216 217 218
    }
  }

219
  @override
220
  AnimationStatus get status => _parent != null ? _parent.status : _status;
221 222

  @override
223
  double get value => _parent != null ? _parent.value : _value;
224 225 226 227 228 229 230

  @override
  String toString() {
    if (parent == null)
      return '$runtimeType(null; ${super.toStringDetails()} ${value.toStringAsFixed(3)})';
    return '$parent\u27A9$runtimeType';
  }
231 232
}

233 234 235 236 237 238 239
/// 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. Notice that 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.
240
class ReverseAnimation extends Animation<double>
241
  with AnimationLazyListenerMixin, AnimationLocalStatusListenersMixin {
242 243 244 245 246 247 248

  /// Creates a reverse animation.
  ///
  /// The parent argument must not be null.
  ReverseAnimation(this.parent) {
    assert(parent != null);
  }
249

250
  /// The animation whose value and direction this animation is reversing.
251
  final Animation<double> parent;
252

253
  @override
254 255
  void addListener(VoidCallback listener) {
    didRegisterListener();
256
    parent.addListener(listener);
257
  }
258 259

  @override
260
  void removeListener(VoidCallback listener) {
261
    parent.removeListener(listener);
262 263 264
    didUnregisterListener();
  }

265
  @override
266
  void didStartListening() {
267
    parent.addStatusListener(_statusChangeHandler);
268 269
  }

270
  @override
271
  void didStopListening() {
272
    parent.removeStatusListener(_statusChangeHandler);
273 274
  }

275
  void _statusChangeHandler(AnimationStatus status) {
276 277 278
    notifyStatusListeners(_reverseStatus(status));
  }

279
  @override
280
  AnimationStatus get status => _reverseStatus(parent.status);
281 282

  @override
283
  double get value => 1.0 - parent.value;
284

285
  AnimationStatus _reverseStatus(AnimationStatus status) {
286
    assert(status != null);
287
    switch (status) {
288 289 290 291
      case AnimationStatus.forward: return AnimationStatus.reverse;
      case AnimationStatus.reverse: return AnimationStatus.forward;
      case AnimationStatus.completed: return AnimationStatus.dismissed;
      case AnimationStatus.dismissed: return AnimationStatus.completed;
292
    }
pq's avatar
pq committed
293
    return null;
294
  }
295 296 297 298 299

  @override
  String toString() {
    return '$parent\u27AA$runtimeType';
  }
300 301
}

302 303
/// An animation that applies a curve to another animation.
///
304 305 306
/// [CurvedAnimation] is useful when you want to apply a non-linear [Curve] to
/// an animation object wrapped in the [CurvedAnimation].
///
307 308
/// For example, the following code snippet shows how you can apply a curve to a
/// linear animation produced by an [AnimationController]:
309 310 311 312 313 314 315
///
/// ``` dart
///     final AnimationController controller =
///         new AnimationController(duration: const Duration(milliseconds: 500));
///     final CurvedAnimation animation =
///         new CurvedAnimation(parent: controller, curve: Curves.ease);
///```
316 317 318 319
/// 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.
320 321
///
/// If you want to apply a [Curve] to a [Tween], consider using [CurveTween].
322
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
323 324 325
  /// Creates a curved animation.
  ///
  /// The parent and curve arguments must not be null.
326 327
  CurvedAnimation({
    this.parent,
328
    this.curve,
329 330 331 332
    this.reverseCurve
  }) {
    assert(parent != null);
    assert(curve != null);
333 334
    _updateCurveDirection(parent.status);
    parent.addStatusListener(_updateCurveDirection);
335 336
  }

337
  /// The animation to which this animation applies a curve.
338
  @override
339 340 341 342 343 344 345
  final Animation<double> parent;

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

  /// The curve to use in the reverse direction.
  ///
346 347 348 349 350 351 352 353 354 355
  /// 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.
  ///
356 357 358 359 360 361 362 363
  /// 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
  /// a animation is used to animate.
Adam Barth's avatar
Adam Barth committed
364
  AnimationStatus _curveDirection;
365

366
  void _updateCurveDirection(AnimationStatus status) {
367 368 369 370 371 372
    switch (status) {
      case AnimationStatus.dismissed:
      case AnimationStatus.completed:
        _curveDirection = null;
        break;
      case AnimationStatus.forward:
Adam Barth's avatar
Adam Barth committed
373
        _curveDirection ??= AnimationStatus.forward;
374 375
        break;
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
376
        _curveDirection ??= AnimationStatus.reverse;
377 378 379 380
        break;
    }
  }

381 382 383 384
  bool get _useForwardCurve {
    return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse;
  }

385
  @override
386
  double get value {
387
    Curve activeCurve = _useForwardCurve ? curve : reverseCurve;
388 389 390 391 392

    double t = parent.value;
    if (activeCurve == null)
      return t;
    if (t == 0.0 || t == 1.0) {
393 394 395 396 397
      assert(() {
        final double transformedValue = activeCurve.transform(t);
        final double roundedTransformedValue = transformedValue.round().toDouble();
        if (roundedTransformedValue != t) {
          throw new FlutterError(
398
            'Invalid curve endpoint at $t.\n'
399 400 401 402 403 404 405
            '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;
      });
406 407 408 409
      return t;
    }
    return activeCurve.transform(t);
  }
410 411 412 413 414

  @override
  String toString() {
    if (reverseCurve == null)
      return '$parent\u27A9$curve';
415
    if (_useForwardCurve)
416
      return '$parent\u27A9$curve\u2092\u2099/$reverseCurve';
417 418
    return '$parent\u27A9$curve/$reverseCurve\u2092\u2099';
  }
419 420
}

421 422 423 424 425 426 427 428 429 430 431 432
enum _TrainHoppingMode { minimize, maximize }

/// This animation starts by proxying one animation, but can be given a
/// second animation. When their times cross (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, and the second
/// animation becomes the new "first" performance.
///
/// 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.
433
class TrainHoppingAnimation extends Animation<double>
434
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
435 436 437 438 439

  /// Creates a train-hopping animation.
  ///
  /// The current train argument must not be null but the next train argument
  /// can be null.
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
  TrainHoppingAnimation(this._currentTrain, this._nextTrain, { this.onSwitchedTrain }) {
    assert(_currentTrain != null);
    if (_nextTrain != null) {
      if (_currentTrain.value > _nextTrain.value) {
        _mode = _TrainHoppingMode.maximize;
      } else {
        _mode = _TrainHoppingMode.minimize;
        if (_currentTrain.value == _nextTrain.value) {
          _currentTrain = _nextTrain;
          _nextTrain = null;
        }
      }
    }
    _currentTrain.addStatusListener(_statusChangeHandler);
    _currentTrain.addListener(_valueChangeHandler);
455
    _nextTrain?.addListener(_valueChangeHandler);
456 457 458
    assert(_mode != null);
  }

459
  /// The animation that is current driving this animation.
460 461 462
  Animation<double> get currentTrain => _currentTrain;
  Animation<double> _currentTrain;
  Animation<double> _nextTrain;
463 464
  _TrainHoppingMode _mode;

465
  /// Called when this animation switches to be driven by a different animation.
466 467
  VoidCallback onSwitchedTrain;

468 469
  AnimationStatus _lastStatus;
  void _statusChangeHandler(AnimationStatus status) {
470 471 472 473 474 475 476 477
    assert(_currentTrain != null);
    if (status != _lastStatus) {
      notifyListeners();
      _lastStatus = status;
    }
    assert(_lastStatus != null);
  }

478
  @override
479
  AnimationStatus get status => _currentTrain.status;
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494

  double _lastValue;
  void _valueChangeHandler() {
    assert(_currentTrain != null);
    bool hop = false;
    if (_nextTrain != null) {
      switch (_mode) {
        case _TrainHoppingMode.minimize:
          hop = _nextTrain.value <= _currentTrain.value;
          break;
        case _TrainHoppingMode.maximize:
          hop = _nextTrain.value >= _currentTrain.value;
          break;
      }
      if (hop) {
495 496 497
        _currentTrain
          ..removeStatusListener(_statusChangeHandler)
          ..removeListener(_valueChangeHandler);
498
        _currentTrain = _nextTrain;
499 500 501
        _nextTrain = null;
        _currentTrain.addStatusListener(_statusChangeHandler);
        _statusChangeHandler(_currentTrain.status);
502 503 504 505 506 507 508 509 510 511 512 513
      }
    }
    double newValue = value;
    if (newValue != _lastValue) {
      notifyListeners();
      _lastValue = newValue;
    }
    assert(_lastValue != null);
    if (hop && onSwitchedTrain != null)
      onSwitchedTrain();
  }

514
  @override
515 516 517 518
  double get value => _currentTrain.value;

  /// Frees all the resources used by this performance.
  /// After this is called, this object is no longer usable.
519
  @override
520 521 522 523 524
  void dispose() {
    assert(_currentTrain != null);
    _currentTrain.removeStatusListener(_statusChangeHandler);
    _currentTrain.removeListener(_valueChangeHandler);
    _currentTrain = null;
525 526
    _nextTrain?.removeListener(_valueChangeHandler);
    _nextTrain = null;
527
    super.dispose();
528
  }
529 530 531 532 533 534 535

  @override
  String toString() {
    if (_nextTrain != null)
      return '$currentTrain\u27A9$runtimeType(next: $_nextTrain)';
    return '$currentTrain\u27A9$runtimeType(no next)';
  }
536
}
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608

/// 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;`
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({
    this.first,
    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);
  }

  @override
  AnimationStatus get status {
    // If one of the sub-animations is moving, use that status. Otherwise,
    // default to `first`.
    if (next.status == AnimationStatus.forward || next.status == AnimationStatus.reverse)
      return next.status;
    return first.status;
  }

  @override
  String toString() {
    return '$runtimeType($first, $next)';
  }

  AnimationStatus _lastStatus;
  void _maybeNotifyStatusListeners(AnimationStatus _) {
    if (this.status != _lastStatus) {
      _lastStatus = this.status;
      notifyStatusListeners(this.status);
    }
  }

  T _lastValue;
  void _maybeNotifyListeners() {
    if (this.value != _lastValue) {
      _lastValue = this.value;
      notifyListeners();
    }
  }
}
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626

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