Skip to content

Commit

Permalink
Enhanced enum features for AnimationStatus (#147801)
Browse files Browse the repository at this point in the history
Based on issue #147799, this pull request adds two `AnimationStatus` getters.

```dart
bool get isRunning => switch (this) {
  forward   || reverse   => true,
  completed || dismissed => false,
};

bool get aimedForward => switch (this) {
  forward || completed => true,
  reverse || dismissed => false,
};
```

I also added a `.toggle()` method for animation controllers that makes use of `aimedForward`.
  • Loading branch information
nate-thegrate authored May 16, 2024
1 parent c719f03 commit 6b38361
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 3 deletions.
36 changes: 33 additions & 3 deletions packages/flutter/lib/src/animation/animation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,31 @@ enum AnimationStatus {
reverse,

/// The animation is stopped at the end.
completed,
completed;

/// Whether the animation is stopped at the beginning.
bool get isDismissed => this == dismissed;

/// Whether the animation is stopped at the end.
bool get isCompleted => this == completed;

/// Whether the animation is running in either direction.
bool get isRunning => switch (this) {
forward || reverse => true,
completed || dismissed => false,
};

/// {@template flutter.animation.AnimationStatus.isForwardOrCompleted}
/// Whether the current aim of the animation is toward completion.
///
/// Specifically, returns `true` for [AnimationStatus.forward] or
/// [AnimationStatus.completed], and `false` for
/// [AnimationStatus.reverse] or [AnimationStatus.dismissed].
/// {@endtemplate}
bool get isForwardOrCompleted => switch (this) {
forward || completed => true,
reverse || dismissed => false,
};
}

/// Signature for listeners attached using [Animation.addStatusListener].
Expand Down Expand Up @@ -150,10 +174,16 @@ abstract class Animation<T> extends Listenable implements ValueListenable<T> {
T get value;

/// Whether this animation is stopped at the beginning.
bool get isDismissed => status == AnimationStatus.dismissed;
bool get isDismissed => status.isDismissed;

/// Whether this animation is stopped at the end.
bool get isCompleted => status == AnimationStatus.completed;
bool get isCompleted => status.isCompleted;

/// Whether this animation is running in either direction.
bool get isRunning => status.isRunning;

/// {@macro flutter.animation.AnimationStatus.isForwardOrCompleted}
bool get isForwardOrCompleted => status.isForwardOrCompleted;

/// Chains a [Tween] (or [CurveTween]) to this [Animation].
///
Expand Down
48 changes: 48 additions & 0 deletions packages/flutter/lib/src/animation/animation_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ class AnimationController extends Animation<double>
///
/// Returns a [TickerFuture] that completes when the animation is complete.
///
/// If [from] is non-null, it will be set as the current [value] before running
/// the animation.
///
/// The most recently returned [TickerFuture], if any, is marked as having been
/// canceled, meaning the future never completes and its [TickerFuture.orCancel]
/// derivative future completes with a [TickerCanceled] error.
Expand Down Expand Up @@ -497,6 +500,9 @@ class AnimationController extends Animation<double>
///
/// Returns a [TickerFuture] that completes when the animation is dismissed.
///
/// If [from] is non-null, it will be set as the current [value] before running
/// the animation.
///
/// The most recently returned [TickerFuture], if any, is marked as having been
/// canceled, meaning the future never completes and its [TickerFuture.orCancel]
/// derivative future completes with a [TickerCanceled] error.
Expand Down Expand Up @@ -527,6 +533,48 @@ class AnimationController extends Animation<double>
return _animateToInternal(lowerBound);
}

/// Toggles the direction of this animation, based on whether it [isForwardOrCompleted].
///
/// Specifically, this function acts the same way as [reverse] if the [status] is
/// either [AnimationStatus.forward] or [AnimationStatus.completed], and acts as
/// [forward] for [AnimationStatus.reverse] or [AnimationStatus.dismissed].
///
/// If [from] is non-null, it will be set as the current [value] before running
/// the animation.
///
/// The most recently returned [TickerFuture], if any, is marked as having been
/// canceled, meaning the future never completes and its [TickerFuture.orCancel]
/// derivative future completes with a [TickerCanceled] error.
TickerFuture toggle({ double? from }) {
assert(() {
Duration? duration = this.duration;
if (isForwardOrCompleted) {
duration ??= reverseDuration;
}
if (duration == null) {
throw FlutterError(
'AnimationController.toggle() called with no default duration.\n'
'The "duration" property should be set, either in the constructor or later, before '
'calling the toggle() function.',
);
}
return true;
}());
assert(
_ticker != null,
'AnimationController.toggle() called after AnimationController.dispose()\n'
'AnimationController methods should not be used after calling dispose.',
);
_direction = isForwardOrCompleted ? _AnimationDirection.reverse : _AnimationDirection.forward;
if (from != null) {
value = from;
}
return _animateToInternal(switch (_direction) {
_AnimationDirection.forward => upperBound,
_AnimationDirection.reverse => lowerBound,
});
}

/// Drives the animation from its current value to target.
///
/// Returns a [TickerFuture] that completes when the animation is complete.
Expand Down
94 changes: 94 additions & 0 deletions packages/flutter/test/animation/animation_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ void main() {
expect(controller.value, moreOrLessEquals(0.0));
controller.stop();

controller.dispose();

// Swap which duration is longer.
controller = AnimationController(
duration: const Duration(milliseconds: 50),
Expand Down Expand Up @@ -220,6 +222,98 @@ void main() {
controller.dispose();
});

test('toggle() with different durations', () {
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),
reverseDuration: const Duration(milliseconds: 50),
vsync: const TestVSync(),
);

controller.toggle();
tick(const Duration(milliseconds: 10));
tick(const Duration(milliseconds: 30));
expect(controller.value, moreOrLessEquals(0.2));
tick(const Duration(milliseconds: 60));
expect(controller.value, moreOrLessEquals(0.5));
tick(const Duration(milliseconds: 90));
expect(controller.value, moreOrLessEquals(0.8));
tick(const Duration(milliseconds: 120));
expect(controller.value, moreOrLessEquals(1.0));
controller.stop();

controller.toggle();
tick(const Duration(milliseconds: 210));
tick(const Duration(milliseconds: 220));
expect(controller.value, moreOrLessEquals(0.8));
tick(const Duration(milliseconds: 230));
expect(controller.value, moreOrLessEquals(0.6));
tick(const Duration(milliseconds: 240));
expect(controller.value, moreOrLessEquals(0.4));
tick(const Duration(milliseconds: 260));
expect(controller.value, moreOrLessEquals(0.0));
controller.stop();

controller.dispose();

// Swap which duration is longer.
controller = AnimationController(
duration: const Duration(milliseconds: 50),
reverseDuration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);

controller.toggle();
tick(const Duration(milliseconds: 10));
tick(const Duration(milliseconds: 30));
expect(controller.value, moreOrLessEquals(0.4));
tick(const Duration(milliseconds: 60));
expect(controller.value, moreOrLessEquals(1.0));
tick(const Duration(milliseconds: 90));
expect(controller.value, moreOrLessEquals(1.0));
controller.stop();

controller.toggle();
tick(const Duration(milliseconds: 210));
tick(const Duration(milliseconds: 220));
expect(controller.value, moreOrLessEquals(0.9));
tick(const Duration(milliseconds: 230));
expect(controller.value, moreOrLessEquals(0.8));
tick(const Duration(milliseconds: 240));
expect(controller.value, moreOrLessEquals(0.7));
tick(const Duration(milliseconds: 260));
expect(controller.value, moreOrLessEquals(0.5));
tick(const Duration(milliseconds: 310));
expect(controller.value, moreOrLessEquals(0.0));
controller.stop();
controller.dispose();
});

test('toggle() acts correctly based on the animation state', () {
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),
reverseDuration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);

controller.forward();
expect(controller.status, AnimationStatus.forward);
expect(controller.isForwardOrCompleted, true);
tick(const Duration(milliseconds: 10));
tick(const Duration(milliseconds: 60));
expect(controller.value, moreOrLessEquals(0.5));
expect(controller.isForwardOrCompleted, true);
controller.toggle();
tick(const Duration(milliseconds: 10));
expect(controller.status, AnimationStatus.reverse);
expect(controller.isForwardOrCompleted, false);
tick(const Duration(milliseconds: 110));
expect(controller.value, moreOrLessEquals(0));
expect(controller.status, AnimationStatus.dismissed);
expect(controller.isForwardOrCompleted, false);

controller.dispose();
});

test('Forward only from value', () {
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),
Expand Down

0 comments on commit 6b38361

Please sign in to comment.