Skip to content

Commit

Permalink
feat: enhance AnimatedTextController with user control states and upd…
Browse files Browse the repository at this point in the history
…ate documentation
  • Loading branch information
oriventi committed Dec 12, 2024
1 parent b11496a commit d2bad09
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 23 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,12 @@ AnimatedTextKit(
speed: const Duration(milliseconds: 2000),
),
],
totalRepeatCount: 4,
pause: const Duration(milliseconds: 1000),
displayFullTextOnTap: true,
stopPauseOnTap: true,
controller: myAnimatedTextController
)
```

Expand All @@ -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:

Expand Down
15 changes: 15 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ class _MyHomePageState extends State<MyHomePage> {
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) {
Expand Down
35 changes: 20 additions & 15 deletions lib/src/animated_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -168,16 +170,12 @@ class _AnimatedTextKitState extends State<AnimatedTextKit>
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;
}
}

Expand All @@ -186,6 +184,7 @@ class _AnimatedTextKitState extends State<AnimatedTextKit>
_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();
}
Expand Down Expand Up @@ -213,8 +212,6 @@ class _AnimatedTextKitState extends State<AnimatedTextKit>
void _nextAnimation() {
final isLast = _isLast;

_animatedTextController.state = AnimatedTextState.playing;

// Handling onNext callback
widget.onNext?.call(_index, isLast);

Expand Down Expand Up @@ -252,10 +249,18 @@ class _AnimatedTextKitState extends State<AnimatedTextKit>

_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() {
Expand Down
57 changes: 52 additions & 5 deletions lib/src/animated_text_controller.dart
Original file line number Diff line number Diff line change
@@ -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<AnimatedTextState> stateNotifier =
ValueNotifier<AnimatedTextState>(AnimatedTextState.stopped);
ValueNotifier<AnimatedTextState>(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;
}
Expand Down
1 change: 0 additions & 1 deletion lib/src/fade.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class FadeAnimatedText extends AnimatedText {
curve: Interval(0.0, fadeInEnd, curve: Curves.linear),
),
);

_fadeOut = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: controller,
Expand Down
58 changes: 58 additions & 0 deletions test/controller_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
}
2 changes: 1 addition & 1 deletion test/smoke_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down

0 comments on commit d2bad09

Please sign in to comment.