diff --git a/packages/mix/lib/src/animation/style_animation_builder.dart b/packages/mix/lib/src/animation/style_animation_builder.dart index 5a833d5cc..536a75067 100644 --- a/packages/mix/lib/src/animation/style_animation_builder.dart +++ b/packages/mix/lib/src/animation/style_animation_builder.dart @@ -78,6 +78,19 @@ class _StyleAnimationBuilderState> }; } + /// 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(); @@ -96,7 +109,7 @@ class _StyleAnimationBuilderState> } else { animationDriver.dispose(); animationDriver = _createAnimationDriver( - config: config ?? oldConfig, + config: config ?? _fallbackConfig(oldConfig), initialSpec: oldWidget.spec, ); } diff --git a/packages/mix/test/src/animation/style_animation_builder_test.dart b/packages/mix/test/src/animation/style_animation_builder_test.dart index e22cbad7e..82da21329 100644 --- a/packages/mix/test/src/animation/style_animation_builder_test.dart +++ b/packages/mix/test/src/animation/style_animation_builder_test.dart @@ -376,6 +376,68 @@ void main() { }, ); + group('dropping a looping animation config', () { + Color? boxColor(WidgetTester tester) { + final decoratedBox = tester.widget( + 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('t', [ + Keyframe.linear(1, const Duration(milliseconds: 1000)), + ], initial: 0), + ], + styleBuilder: (values, style) => style.color( + Color.lerp(Colors.red, Colors.green, values.get('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( + 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(