Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/mix/lib/src/animation/style_animation_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ class _StyleAnimationBuilderState<S extends Spec<S>>
};
}

/// The config to keep using when a style drops its animation config.
///
/// Implicit configs (curve, spring) keep animating toward the new spec, so a
/// variant that removes its animation still transitions back smoothly.
/// Keyframe and phase configs produce their own specs and may loop forever,
/// so they are dropped and the new spec is applied directly.
static AnimationConfig? _fallbackConfig(AnimationConfig? oldConfig) {
return switch (oldConfig) {
CurveAnimationConfig() || SpringAnimationConfig() => oldConfig,
_ => null,
};
}

@override
void dispose() {
animationDriver.dispose();
Expand All @@ -96,7 +109,7 @@ class _StyleAnimationBuilderState<S extends Spec<S>>
} else {
animationDriver.dispose();
animationDriver = _createAnimationDriver(
config: config ?? oldConfig,
config: config ?? _fallbackConfig(oldConfig),
initialSpec: oldWidget.spec,
);
}
Expand Down
62 changes: 62 additions & 0 deletions packages/mix/test/src/animation/style_animation_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,68 @@ void main() {
},
);

group('dropping a looping animation config', () {
Color? boxColor(WidgetTester tester) {
final decoratedBox = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(Box),
matching: find.byType(DecoratedBox),
),
);

return (decoratedBox.decoration as BoxDecoration).color;
}

testWidgets('stops a looping keyframe animation', (tester) async {
final loopingStyle = BoxStyler().keyframeAnimation(
timeline: [
KeyframeTrack<double>('t', [
Keyframe.linear(1, const Duration(milliseconds: 1000)),
], initial: 0),
],
styleBuilder: (values, style) => style.color(
Color.lerp(Colors.red, Colors.green, values.get<double>('t'))!,
),
);
final plainStyle = BoxStyler().color(Colors.blue);

await tester.pumpWidget(MaterialApp(home: Box(style: loopingStyle)));
await tester.pump(const Duration(milliseconds: 300));
expect(tester.hasRunningAnimations, isTrue);

await tester.pumpWidget(MaterialApp(home: Box(style: plainStyle)));
await tester.pump();

expect(tester.hasRunningAnimations, isFalse);
expect(boxColor(tester), Colors.blue);

await tester.pump(const Duration(milliseconds: 500));
expect(boxColor(tester), Colors.blue);
});

testWidgets('stops a looping phase animation', (tester) async {
final loopingStyle = BoxStyler().phaseAnimation<Color>(
phases: const [Colors.red, Colors.green],
styleBuilder: (color, style) => style.color(color),
configBuilder: (_) => const CurveAnimationConfig(
duration: Duration(milliseconds: 500),
curve: Curves.linear,
),
);
final plainStyle = BoxStyler().color(Colors.blue);

await tester.pumpWidget(MaterialApp(home: Box(style: loopingStyle)));
await tester.pump(const Duration(milliseconds: 300));
expect(tester.hasRunningAnimations, isTrue);

await tester.pumpWidget(MaterialApp(home: Box(style: plainStyle)));
await tester.pump();

expect(tester.hasRunningAnimations, isFalse);
expect(boxColor(tester), Colors.blue);
});
});

testWidgets('handles null animation value gracefully', (tester) async {
// Create spec with animation that produces null value
const animationConfig = CurveAnimationConfig(
Expand Down
Loading