From 6b383615c655fe4e58643992a46df31b1f91da52 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 16 May 2024 11:57:53 -0600 Subject: [PATCH] Enhanced enum features for `AnimationStatus` (#147801) 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`. --- .../flutter/lib/src/animation/animation.dart | 36 ++++++- .../src/animation/animation_controller.dart | 48 ++++++++++ .../animation/animation_controller_test.dart | 94 +++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/animation/animation.dart b/packages/flutter/lib/src/animation/animation.dart index d7e9a024fccf1..c542ce5eb1c83 100644 --- a/packages/flutter/lib/src/animation/animation.dart +++ b/packages/flutter/lib/src/animation/animation.dart @@ -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]. @@ -150,10 +174,16 @@ abstract class Animation extends Listenable implements ValueListenable { 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]. /// diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index eb3affccbaa2c..8546d5db69ca1 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -463,6 +463,9 @@ class AnimationController extends Animation /// /// 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. @@ -497,6 +500,9 @@ class AnimationController extends Animation /// /// 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. @@ -527,6 +533,48 @@ class AnimationController extends Animation 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. diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart index c2112762946d3..76dffa95b5be3 100644 --- a/packages/flutter/test/animation/animation_controller_test.dart +++ b/packages/flutter/test/animation/animation_controller_test.dart @@ -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), @@ -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),