diff --git a/README.md b/README.md index b18288a..b2fe4e8 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,12 @@ AnimatedTextKit( speed: const Duration(milliseconds: 2000), ), ], - + totalRepeatCount: 4, pause: const Duration(milliseconds: 1000), displayFullTextOnTap: true, stopPauseOnTap: true, + controller: myAnimatedTextController ) ``` @@ -134,6 +135,7 @@ It has many configurable properties, including: - `isRepeatingAnimation` – controls whether the animation repeats - `repeatForever` – controls whether the animation repeats forever - `totalRepeatCount` – number of times the animation should repeat (when `repeatForever` is `false`) +- `controller` - It allows for control over the animation by providing methods to play, pause and reset the text animations programmatically There are also custom callbacks: diff --git a/example/lib/main.dart b/example/lib/main.dart index af401e4..fa83ee1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -82,6 +82,21 @@ class _MyHomePageState extends State { floatingActionButton: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + FloatingActionButton( + onPressed: () { + animatedTextExample.controller.reset(); + setState(() { + _isAnimationPaused = false; + _tapCount = 0; + }); + }, + tooltip: 'Reset current animation', + child: const Icon( + Icons.replay_sharp, + size: 50.0, + ), + ), + const SizedBox(width: 16), FloatingActionButton( onPressed: () { if (_isAnimationPaused) { diff --git a/lib/src/animated_text.dart b/lib/src/animated_text.dart index 52af48e..f97afb4 100644 --- a/lib/src/animated_text.dart +++ b/lib/src/animated_text.dart @@ -112,10 +112,12 @@ class AnimatedTextKit extends StatefulWidget { /// By default it is set to 3 final int totalRepeatCount; - /// The controller for the animation. - /// This can be used to control the animation. - /// For example, you can pause, play, or reset the animation, by calling - /// [play()], [pause()], or [reset()] on the controller. + /// A controller for managing the state of an animated text sequence. + /// + /// This controller exposes methods to play, pause, and reset the animation. + /// The [AnimatedTextState] enum represents the various states the animation + /// can be in. By calling [play()], [pause()], or [reset()], you can transition + /// between these states and the animated widget will react accordingly. final AnimatedTextController? controller; const AnimatedTextKit({ @@ -168,16 +170,12 @@ class _AnimatedTextKitState extends State if (!mounted) return; if (_animatedTextController.state == AnimatedTextState.playing && !_controller.isAnimating) { - debugPrint('Playing'); _controller.forward(); - } else if (_animatedTextController.state == AnimatedTextState.paused) { - debugPrint('Pausing'); + } else if (_animatedTextController.state == AnimatedTextState.userPaused) { _controller.stop(); } else if (_animatedTextController.state == AnimatedTextState.reset) { - debugPrint('Resetting'); _controller.reset(); - } else { - debugPrint('Unknown state: ${_animatedTextController.state}'); + _animatedTextController.state = AnimatedTextState.playing; } } @@ -186,6 +184,7 @@ class _AnimatedTextKitState extends State _timer?.cancel(); _controller.dispose(); _animatedTextController.stateNotifier.removeListener(_stateChangedCallback); + // Only dispose the controller if it was created by this widget if (widget.controller == null) _animatedTextController.dispose(); super.dispose(); } @@ -213,8 +212,6 @@ class _AnimatedTextKitState extends State void _nextAnimation() { final isLast = _isLast; - _animatedTextController.state = AnimatedTextState.playing; - // Handling onNext callback widget.onNext?.call(_index, isLast); @@ -252,10 +249,18 @@ class _AnimatedTextKitState extends State _currentAnimatedText.initAnimation(_controller); - _controller - ..addStatusListener(_animationEndCallback) - ..forward(); + _controller.addStatusListener(_animationEndCallback); + + if (_animatedTextController.state == + AnimatedTextState.pausingBetweenAnimationsWithUserPauseRequested) { + // This post frame callback is needed to ensure that the state is set and the widget is built + // before we pause the animation. otherwise nothing will be shown during the animation cycle + WidgetsBinding.instance.addPostFrameCallback((_) { + _animatedTextController.state = AnimatedTextState.userPaused; + }); + } _animatedTextController.state = AnimatedTextState.playing; + _controller.forward(); } void _setPauseBetweenAnimations() { diff --git a/lib/src/animated_text_controller.dart b/lib/src/animated_text_controller.dart index a86d493..99f9372 100644 --- a/lib/src/animated_text_controller.dart +++ b/lib/src/animated_text_controller.dart @@ -1,36 +1,83 @@ import 'package:flutter/material.dart'; +/// The various states that the animated text can be in: +/// +/// * [playing]: The animation is currently running. +/// * [userPaused]: The animation is paused due to a user action. +/// * [pausingBetweenAnimations]: The animation has completed one segment and is +/// currently in the built-in pause period before the next segment starts. +/// * [pausingBetweenAnimationsWithUserPauseRequested]: The user requested a pause +/// during the pause between animations, so once this pause period ends, +/// the animation should remain paused. +/// * [stopped]: The animation is stopped and will not progress further. +/// * [reset]: The animation should reset to its initial state. enum AnimatedTextState { playing, - paused, + userPaused, pausingBetweenAnimations, + pausingBetweenAnimationsWithUserPauseRequested, stopped, reset, } -//TODO: fix bug where the animation is not paused, when state is pausingBetweenAnimations +/// A controller for managing the state of an animated text sequence. +/// +/// This controller exposes methods to play, pause, and reset the animation. +/// The [AnimatedTextState] enum represents the various states the animation +/// can be in. By calling [play()], [pause()], or [reset()], you can transition +/// between these states and the animated widget will react accordingly. class AnimatedTextController { + /// A [ValueNotifier] that holds the current state of the animation. + /// Listeners can be attached to react when the state changes. final ValueNotifier stateNotifier = - ValueNotifier(AnimatedTextState.stopped); + ValueNotifier(AnimatedTextState.playing); + /// Returns the current state of the animation. AnimatedTextState get state => stateNotifier.value; + /// Sets the current state of the animation. set state(AnimatedTextState state) { stateNotifier.value = state; } + /// Disposes of the [ValueNotifier]. This should be called when the + /// [AnimatedTextController] is no longer needed. void dispose() { stateNotifier.dispose(); } + /// Transitions the animation into the [playing] state, unless the controller is + /// currently in the [pausingBetweenAnimationsWithUserPauseRequested] state, + /// in which case it returns to the [pausingBetweenAnimations] state. + /// + /// Call this to resume the animation if it was previously paused. void play() { - stateNotifier.value = AnimatedTextState.playing; + if (stateNotifier.value == + AnimatedTextState.pausingBetweenAnimationsWithUserPauseRequested) { + stateNotifier.value = AnimatedTextState.pausingBetweenAnimations; + } else { + stateNotifier.value = AnimatedTextState.playing; + } } + /// Pauses the animation. If the animation is currently in the [pausingBetweenAnimations] + /// state, it moves to [pausingBetweenAnimationsWithUserPauseRequested], indicating + /// that once the internal pause finishes, the animation should remain paused. + /// Otherwise, it transitions directly into the [userPaused] state. + /// + /// Call this to pause the animation due to user interaction. void pause() { - stateNotifier.value = AnimatedTextState.paused; + if (stateNotifier.value == AnimatedTextState.pausingBetweenAnimations) { + stateNotifier.value = + AnimatedTextState.pausingBetweenAnimationsWithUserPauseRequested; + } else { + stateNotifier.value = AnimatedTextState.userPaused; + } } + /// Resets the animation to its initial state by setting the state to [reset]. + /// This typically means the animated text should return to the start of its + /// animation in this cycle and be ready to begin again. void reset() { stateNotifier.value = AnimatedTextState.reset; } diff --git a/lib/src/fade.dart b/lib/src/fade.dart index 3600afb..e819890 100644 --- a/lib/src/fade.dart +++ b/lib/src/fade.dart @@ -36,7 +36,6 @@ class FadeAnimatedText extends AnimatedText { curve: Interval(0.0, fadeInEnd, curve: Curves.linear), ), ); - _fadeOut = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation( parent: controller, diff --git a/test/controller_test.dart b/test/controller_test.dart new file mode 100644 index 0000000..1f6d29d --- /dev/null +++ b/test/controller_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:animated_text_kit/src/animated_text_controller.dart'; + +void main() { + late AnimatedTextController controller; + + setUp(() { + controller = AnimatedTextController(); + }); + + tearDown(() { + controller.dispose(); + }); + + test('Initial state should be playing', () { + expect(controller.state, AnimatedTextState.playing); + }); + + test('Calling pause when playing should set state to userPaused', () { + controller.pause(); + expect(controller.state, AnimatedTextState.userPaused); + }); + + test('Calling play after paused should set state to playing', () { + controller.pause(); // userPaused + controller.play(); + expect(controller.state, AnimatedTextState.playing); + }); + + test( + 'Pausing during pausingBetweenAnimations should set state to pausingBetweenAnimationsWithUserPauseRequested', + () { + // Directly set state to pausingBetweenAnimations to simulate this scenario. + controller.state = AnimatedTextState.pausingBetweenAnimations; + controller.pause(); + expect(controller.state, + AnimatedTextState.pausingBetweenAnimationsWithUserPauseRequested); + }); + + test( + 'Calling play when in pausingBetweenAnimationsWithUserPauseRequested should revert to pausingBetweenAnimations', + () { + controller.state = + AnimatedTextState.pausingBetweenAnimationsWithUserPauseRequested; + controller.play(); + expect(controller.state, AnimatedTextState.pausingBetweenAnimations); + }); + + test('Resetting should set state to reset', () { + controller.reset(); + expect(controller.state, AnimatedTextState.reset); + }); + + test('Changing state directly via setter works', () { + controller.state = AnimatedTextState.userPaused; + expect(controller.state, AnimatedTextState.userPaused); + }); +} diff --git a/test/smoke_test.dart b/test/smoke_test.dart index c9772f3..e2b8399 100644 --- a/test/smoke_test.dart +++ b/test/smoke_test.dart @@ -18,7 +18,7 @@ void main() { final pumpCount = await tester.pumpAndSettle(); print(' > ${example.label} pumped $pumpCount'); - await tester.tap(find.byIcon(Icons.play_circle_filled)); + await tester.tap(find.byIcon(Icons.arrow_right)); await tester.pump(); }