From 7ece60bc70b7f834ab03e804d16d92146102878b Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Wed, 17 Jun 2026 23:15:50 -0300 Subject: [PATCH] feat(mix): add reverse animation config for style exit transitions Add an optional `reverse` parameter to `animate` so a variant style can own both its enter and exit transition configs: BoxStyler() .animate(baseAnimation) .onHovered(BoxStyler().scale(1.02).animate(hoverIn, reverse: hoverOut)); When a style becomes active, Mix uses the target style's forward config; when it is no longer active, Mix uses the leaving style's reverse config. The reverse config is only used when the old and new animation metadata are distinct, so an inherited base reversible config is not mistaken for an exit animation. - Add `ReversibleAnimationConfig` wrapping forward + reverse configs. - `StyleAnimationBuilder` selects the effective transition config from old/new metadata and compares against the active driver kind (so forward and reverse may use different driver kinds). - Generated `animate` methods and static factories accept `{AnimationConfig? reverse}` and wrap only when reverse is provided. Closes #929 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/src/animation/animation_config.dart | 22 +++ .../animation/style_animation_builder.dart | 62 ++++++++- .../mix/lib/src/specs/box/box_spec.g.dart | 17 ++- .../mix/lib/src/specs/flex/flex_spec.g.dart | 11 +- .../lib/src/specs/flexbox/flexbox_spec.g.dart | 17 ++- .../mix/lib/src/specs/icon/icon_spec.g.dart | 11 +- .../mix/lib/src/specs/image/image_spec.g.dart | 11 +- .../mix/lib/src/specs/stack/stack_spec.g.dart | 11 +- .../src/specs/stackbox/stackbox_spec.g.dart | 17 ++- .../mix/lib/src/specs/text/text_spec.g.dart | 11 +- .../style/mixins/animation_style_mixin.dart | 2 +- .../src/animation/animation_config_test.dart | 131 ++++++++++++++++++ .../src/core/style_builder_hover_test.dart | 94 +++++++++++++ .../core/builders/styler_mixin_builder.dart | 17 ++- .../core/curated/styler_surface_metadata.dart | 4 +- .../builders/styler_mixin_builder_test.dart | 15 +- .../spec_styler_generator_smoke_test.dart | 21 +-- 17 files changed, 432 insertions(+), 42 deletions(-) diff --git a/packages/mix/lib/src/animation/animation_config.dart b/packages/mix/lib/src/animation/animation_config.dart index a70f54e027..f18a9afba2 100644 --- a/packages/mix/lib/src/animation/animation_config.dart +++ b/packages/mix/lib/src/animation/animation_config.dart @@ -427,6 +427,28 @@ sealed class AnimationConfig { ); } +/// Animation configuration that pairs a forward (enter) config with a reverse +/// (exit) config. +/// +/// This wrapper lets a single style own both its enter and exit transition +/// timing. When a style becomes active, Mix uses [forward]; when the style is +/// no longer active, Mix uses the leaving style's [reverse]. +/// +/// `reverse` is an exit transition config. It does not mean calling +/// `AnimationController.reverse()`. +final class ReversibleAnimationConfig extends AnimationConfig with Equatable { + final AnimationConfig forward; + final AnimationConfig reverse; + + const ReversibleAnimationConfig({ + required this.forward, + required this.reverse, + }); + + @override + List get props => [forward, reverse]; +} + /// Curve-based animation configuration with fixed duration. /// /// This configuration provides duration, curve, and optional completion callback diff --git a/packages/mix/lib/src/animation/style_animation_builder.dart b/packages/mix/lib/src/animation/style_animation_builder.dart index 5a833d5ccf..f23dc92b77 100644 --- a/packages/mix/lib/src/animation/style_animation_builder.dart +++ b/packages/mix/lib/src/animation/style_animation_builder.dart @@ -34,12 +34,18 @@ class _StyleAnimationBuilderState> with TickerProviderStateMixin { late StyleAnimationDriver animationDriver; + /// The config currently driving the active driver. Tracked separately from + /// the spec's config because a reverse transition can leave the driver using + /// a different kind from the current spec's forward config. + AnimationConfig? _activeConfig; + @override void initState() { super.initState(); final spec = widget.spec; - final config = spec.animation; + final config = _forwardOf(spec.animation); animationDriver = _createAnimationDriver(config: config, initialSpec: spec); + _activeConfig = config; } StyleAnimationDriver _createAnimationDriver({ @@ -73,6 +79,11 @@ class _StyleAnimationBuilderState> initialSpec: initialSpec, context: context, ), + // A reversible config is unwrapped to its forward config for driving. + ReversibleAnimationConfig() => _createAnimationDriver( + config: config.forward, + initialSpec: initialSpec, + ), // ignore: avoid-undisposed-instances null => NoAnimationDriver(vsync: this, initialSpec: initialSpec), }; @@ -88,18 +99,26 @@ class _StyleAnimationBuilderState> void didUpdateWidget(StyleAnimationBuilder oldWidget) { super.didUpdateWidget(oldWidget); - final config = widget.spec.animation; + final newConfig = widget.spec.animation; final oldConfig = oldWidget.spec.animation; - if ((oldConfig.runtimeType == config.runtimeType) && config != null) { + // Select the effective transition config: the leaving style's reverse + // config when leaving it, otherwise the target style's forward config. + final config = + _transitionConfig(oldConfig, newConfig) ?? _forwardOf(oldConfig); + + // Compare against the active driver's config kind, not the spec's, because + // a reverse transition can leave the driver using a different kind. + if ((_activeConfig.runtimeType == config.runtimeType) && config != null) { animationDriver.updateDriver(config); } else { animationDriver.dispose(); animationDriver = _createAnimationDriver( - config: config ?? oldConfig, + config: config, initialSpec: oldWidget.spec, ); } + _activeConfig = config; if (oldWidget.spec != widget.spec) { animationDriver.didUpdateSpec(oldWidget.spec, widget.spec); @@ -118,3 +137,38 @@ class _StyleAnimationBuilderState> ); } } + +/// Unwraps the forward (enter) config from a [ReversibleAnimationConfig]. +AnimationConfig? _forwardOf(AnimationConfig? config) { + return switch (config) { + ReversibleAnimationConfig(:final forward) => forward, + _ => config, + }; +} + +/// Unwraps the reverse (exit) config from a [ReversibleAnimationConfig]. +AnimationConfig? _reverseOf(AnimationConfig? config) { + return switch (config) { + ReversibleAnimationConfig(:final reverse) => reverse, + _ => null, + }; +} + +/// Selects the effective transition config when moving from [oldConfig] to +/// [newConfig]. +/// +/// Uses the leaving style's reverse config only when the old and new configs +/// are distinct; otherwise uses the target style's forward config. This keeps +/// an inherited base reversible config from being mistaken for an exit +/// animation on enter. +AnimationConfig? _transitionConfig( + AnimationConfig? oldConfig, + AnimationConfig? newConfig, +) { + if (oldConfig != newConfig) { + final reverse = _reverseOf(oldConfig); + if (reverse != null) return reverse; + } + + return _forwardOf(newConfig); +} diff --git a/packages/mix/lib/src/specs/box/box_spec.g.dart b/packages/mix/lib/src/specs/box/box_spec.g.dart index 1b6c080de9..601bcdcc48 100644 --- a/packages/mix/lib/src/specs/box/box_spec.g.dart +++ b/packages/mix/lib/src/specs/box/box_spec.g.dart @@ -379,8 +379,10 @@ class BoxStyler extends MixStyler ); factory BoxStyler.transform(Matrix4 value, {Alignment alignment = .center}) => BoxStyler().transform(value, alignment: alignment); - factory BoxStyler.animate(AnimationConfig value) => - BoxStyler().animate(value); + factory BoxStyler.animate( + AnimationConfig value, { + AnimationConfig? reverse, + }) => BoxStyler().animate(value, reverse: reverse); BoxStyler textStyle(TextStyler value) { return wrap(WidgetModifierConfig.defaultTextStyler(value)); @@ -432,9 +434,16 @@ class BoxStyler extends MixStyler } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - BoxStyler animate(AnimationConfig value) { - return merge(BoxStyler(animation: value)); + BoxStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(BoxStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/flex/flex_spec.g.dart b/packages/mix/lib/src/specs/flex/flex_spec.g.dart index 19f9ee66cd..ebd19613c8 100644 --- a/packages/mix/lib/src/specs/flex/flex_spec.g.dart +++ b/packages/mix/lib/src/specs/flex/flex_spec.g.dart @@ -296,9 +296,16 @@ class FlexStyler extends MixStyler } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - FlexStyler animate(AnimationConfig value) { - return merge(FlexStyler(animation: value)); + FlexStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(FlexStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/flexbox/flexbox_spec.g.dart b/packages/mix/lib/src/specs/flexbox/flexbox_spec.g.dart index a2ebedd9d8..aa9659373c 100644 --- a/packages/mix/lib/src/specs/flexbox/flexbox_spec.g.dart +++ b/packages/mix/lib/src/specs/flexbox/flexbox_spec.g.dart @@ -358,8 +358,10 @@ class FlexBoxStyler extends MixStyler Matrix4 value, { Alignment alignment = .center, }) => FlexBoxStyler().transform(value, alignment: alignment); - factory FlexBoxStyler.animate(AnimationConfig value) => - FlexBoxStyler().animate(value); + factory FlexBoxStyler.animate( + AnimationConfig value, { + AnimationConfig? reverse, + }) => FlexBoxStyler().animate(value, reverse: reverse); FlexBoxStyler textStyle(TextStyler value) { return wrap(WidgetModifierConfig.defaultTextStyler(value)); @@ -418,9 +420,16 @@ class FlexBoxStyler extends MixStyler } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - FlexBoxStyler animate(AnimationConfig value) { - return merge(FlexBoxStyler(animation: value)); + FlexBoxStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(FlexBoxStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/icon/icon_spec.g.dart b/packages/mix/lib/src/specs/icon/icon_spec.g.dart index f6762df27b..106610d19f 100644 --- a/packages/mix/lib/src/specs/icon/icon_spec.g.dart +++ b/packages/mix/lib/src/specs/icon/icon_spec.g.dart @@ -339,9 +339,16 @@ class IconStyler extends MixStyler { } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - IconStyler animate(AnimationConfig value) { - return merge(IconStyler(animation: value)); + IconStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(IconStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/image/image_spec.g.dart b/packages/mix/lib/src/specs/image/image_spec.g.dart index 7db71f5b81..65eecc2432 100644 --- a/packages/mix/lib/src/specs/image/image_spec.g.dart +++ b/packages/mix/lib/src/specs/image/image_spec.g.dart @@ -392,9 +392,16 @@ class ImageStyler extends MixStyler { } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - ImageStyler animate(AnimationConfig value) { - return merge(ImageStyler(animation: value)); + ImageStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(ImageStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/stack/stack_spec.g.dart b/packages/mix/lib/src/specs/stack/stack_spec.g.dart index 682cc88e69..7f03eb10d5 100644 --- a/packages/mix/lib/src/specs/stack/stack_spec.g.dart +++ b/packages/mix/lib/src/specs/stack/stack_spec.g.dart @@ -165,9 +165,16 @@ class StackStyler extends MixStyler { } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - StackStyler animate(AnimationConfig value) { - return merge(StackStyler(animation: value)); + StackStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(StackStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/stackbox/stackbox_spec.g.dart b/packages/mix/lib/src/specs/stackbox/stackbox_spec.g.dart index 5cb9f22b80..f073d5ece7 100644 --- a/packages/mix/lib/src/specs/stackbox/stackbox_spec.g.dart +++ b/packages/mix/lib/src/specs/stackbox/stackbox_spec.g.dart @@ -340,8 +340,10 @@ class StackBoxStyler extends MixStyler Matrix4 value, { Alignment alignment = .center, }) => StackBoxStyler().transform(value, alignment: alignment); - factory StackBoxStyler.animate(AnimationConfig value) => - StackBoxStyler().animate(value); + factory StackBoxStyler.animate( + AnimationConfig value, { + AnimationConfig? reverse, + }) => StackBoxStyler().animate(value, reverse: reverse); StackBoxStyler textStyle(TextStyler value) { return wrap(WidgetModifierConfig.defaultTextStyler(value)); @@ -415,9 +417,16 @@ class StackBoxStyler extends MixStyler } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - StackBoxStyler animate(AnimationConfig value) { - return merge(StackBoxStyler(animation: value)); + StackBoxStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(StackBoxStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/specs/text/text_spec.g.dart b/packages/mix/lib/src/specs/text/text_spec.g.dart index a2d6293fdf..a55c75b8e2 100644 --- a/packages/mix/lib/src/specs/text/text_spec.g.dart +++ b/packages/mix/lib/src/specs/text/text_spec.g.dart @@ -432,9 +432,16 @@ class TextStyler extends MixStyler } /// Sets the animation configuration. + /// + /// When [reverse] is provided, it is used as the exit transition + /// config when leaving this style. @override - TextStyler animate(AnimationConfig value) { - return merge(TextStyler(animation: value)); + TextStyler animate(AnimationConfig value, {AnimationConfig? reverse}) { + final config = reverse == null + ? value + : ReversibleAnimationConfig(forward: value, reverse: reverse); + + return merge(TextStyler(animation: config)); } /// Sets the style variants. diff --git a/packages/mix/lib/src/style/mixins/animation_style_mixin.dart b/packages/mix/lib/src/style/mixins/animation_style_mixin.dart index 256d9f28db..4833bc8ce0 100644 --- a/packages/mix/lib/src/style/mixins/animation_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/animation_style_mixin.dart @@ -6,7 +6,7 @@ import '../../core/style.dart'; mixin AnimationStyleMixin, S extends Spec> on Style { @protected - T animate(AnimationConfig config); + T animate(AnimationConfig config, {AnimationConfig? reverse}); /// Creates a keyframe animation. It will animate through the given timeline. T keyframeAnimation({ diff --git a/packages/mix/test/src/animation/animation_config_test.dart b/packages/mix/test/src/animation/animation_config_test.dart index d24b18c00b..472c8ae058 100644 --- a/packages/mix/test/src/animation/animation_config_test.dart +++ b/packages/mix/test/src/animation/animation_config_test.dart @@ -655,4 +655,135 @@ void main() { trigger2.dispose(); }); }); + + group('ReversibleAnimationConfig', () { + test('exposes forward and reverse configs', () { + const forward = CurveAnimationConfig( + duration: Duration(milliseconds: 200), + curve: Curves.easeIn, + ); + const reverse = CurveAnimationConfig( + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ); + + const config = ReversibleAnimationConfig( + forward: forward, + reverse: reverse, + ); + + expect(config.forward, forward); + expect(config.reverse, reverse); + expect(config, isA()); + }); + + test('supports equality on forward and reverse', () { + const forward = CurveAnimationConfig( + duration: Duration(milliseconds: 200), + curve: Curves.easeIn, + ); + const reverse = CurveAnimationConfig( + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ); + + const config1 = ReversibleAnimationConfig( + forward: forward, + reverse: reverse, + ); + const config2 = ReversibleAnimationConfig( + forward: forward, + reverse: reverse, + ); + + expect(config1, equals(config2)); + }); + + test('differs when reverse config differs', () { + const forward = CurveAnimationConfig( + duration: Duration(milliseconds: 200), + curve: Curves.easeIn, + ); + + const config1 = ReversibleAnimationConfig( + forward: forward, + reverse: CurveAnimationConfig( + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ), + ); + const config2 = ReversibleAnimationConfig( + forward: forward, + reverse: CurveAnimationConfig( + duration: Duration(milliseconds: 300), + curve: Curves.easeOut, + ), + ); + + expect(config1, isNot(equals(config2))); + }); + + test('allows forward and reverse with different driver kinds', () { + final config = ReversibleAnimationConfig( + forward: SpringAnimationConfig.standard(), + reverse: const CurveAnimationConfig( + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ), + ); + + expect(config.forward, isA()); + expect(config.reverse, isA()); + }); + }); + + group('animate(reverse:)', () { + test('wraps in ReversibleAnimationConfig when reverse is provided', () { + const forward = CurveAnimationConfig( + duration: Duration(milliseconds: 200), + curve: Curves.easeIn, + ); + const reverse = CurveAnimationConfig( + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ); + + final styler = BoxStyler().animate(forward, reverse: reverse); + + expect( + styler.$animation, + const ReversibleAnimationConfig(forward: forward, reverse: reverse), + ); + }); + + test('keeps the raw config when reverse is omitted', () { + const forward = CurveAnimationConfig( + duration: Duration(milliseconds: 200), + curve: Curves.easeIn, + ); + + final styler = BoxStyler().animate(forward); + + expect(styler.$animation, forward); + expect(styler.$animation, isNot(isA())); + }); + + test('static factory supports reverse', () { + const forward = CurveAnimationConfig( + duration: Duration(milliseconds: 200), + curve: Curves.easeIn, + ); + const reverse = CurveAnimationConfig( + duration: Duration(milliseconds: 100), + curve: Curves.easeOut, + ); + + final styler = BoxStyler.animate(forward, reverse: reverse); + + expect( + styler.$animation, + const ReversibleAnimationConfig(forward: forward, reverse: reverse), + ); + }); + }); } diff --git a/packages/mix/test/src/core/style_builder_hover_test.dart b/packages/mix/test/src/core/style_builder_hover_test.dart index 983c9cd11f..9699ecda65 100644 --- a/packages/mix/test/src/core/style_builder_hover_test.dart +++ b/packages/mix/test/src/core/style_builder_hover_test.dart @@ -178,5 +178,99 @@ void main() { await gesture.removePointer(); }, ); + + testWidgets( + 'hover with animate(reverse:) uses forward on enter and reverse on exit', + (tester) async { + // Forward enter is slow (300ms), reverse exit is fast (60ms). We pump + // partway through each to assert the correct timing config drives the + // transition without throwing. + final style = BoxStyler() + .color(Colors.blue) + .width(100) + .height(100) + .animate(.linear(300.ms)) + .onHovered( + BoxStyler() + .color(Colors.red) + .scale(1.02) + .animate(.linear(300.ms), reverse: .linear(60.ms)), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: Box(key: const Key('reverse_card'), style: style), + ), + ), + ), + ); + + final gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(location: Offset.zero); + + // Enter: forward (300ms) drives. Settle the animation fully. + await gesture.moveTo( + tester.getCenter(find.byKey(const Key('reverse_card'))), + ); + await tester.pump(); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + // Exit: reverse (60ms) drives. Pump less than forward duration to make + // sure the fast exit config completed the transition. + await gesture.moveTo(const Offset(-100, -100)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 80)); + expect(tester.takeException(), isNull); + + await tester.pumpAndSettle(); + await gesture.removePointer(); + }, + ); + + testWidgets('inherited base reversible config does not break hover enter', ( + tester, + ) async { + // The base owns a reversible config and the hover style inherits it. + // Enter should use the forward config rather than the reverse one. + final style = BoxStyler() + .color(Colors.blue) + .width(100) + .height(100) + .animate(.linear(120.ms), reverse: .linear(60.ms)) + .onHovered(BoxStyler().color(Colors.red).scale(1.02)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: Box(key: const Key('inherited_card'), style: style), + ), + ), + ), + ); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + + await gesture.moveTo( + tester.getCenter(find.byKey(const Key('inherited_card'))), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 70)); + expect(tester.takeException(), isNull); + + await tester.pumpAndSettle(); + await gesture.moveTo(const Offset(-100, -100)); + await tester.pump(); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + await gesture.removePointer(); + }); }); } diff --git a/packages/mix_generator/lib/src/core/builders/styler_mixin_builder.dart b/packages/mix_generator/lib/src/core/builders/styler_mixin_builder.dart index 6d995d6094..401bc8896e 100644 --- a/packages/mix_generator/lib/src/core/builders/styler_mixin_builder.dart +++ b/packages/mix_generator/lib/src/core/builders/styler_mixin_builder.dart @@ -67,9 +67,22 @@ class StylerMixinBuilder { final buffer = StringBuffer(); buffer.writeln(' /// Sets the animation configuration.'); + buffer.writeln(' ///'); + buffer.writeln( + ' /// When [reverse] is provided, it is used as the exit transition', + ); + buffer.writeln(' /// config when leaving this style.'); _writeMethodOverride(buffer, 'animate', methodOverrides); - buffer.writeln(' $stylerName animate(AnimationConfig value) {'); - buffer.writeln(' return merge($stylerName(animation: value));'); + buffer.writeln( + ' $stylerName animate(AnimationConfig value, {AnimationConfig? reverse}) {', + ); + buffer.writeln(' final config = reverse == null'); + buffer.writeln(' ? value'); + buffer.writeln( + ' : ReversibleAnimationConfig(forward: value, reverse: reverse);', + ); + buffer.writeln(); + buffer.writeln(' return merge($stylerName(animation: config));'); buffer.writeln(' }'); buffer.writeln(); diff --git a/packages/mix_generator/lib/src/core/curated/styler_surface_metadata.dart b/packages/mix_generator/lib/src/core/curated/styler_surface_metadata.dart index 515f574352..47f29cf87b 100644 --- a/packages/mix_generator/lib/src/core/curated/styler_surface_metadata.dart +++ b/packages/mix_generator/lib/src/core/curated/styler_surface_metadata.dart @@ -884,7 +884,7 @@ StylerMethodDescriptor transformAnchorMethodDescriptor() { StylerFactoryDescriptor animateFactoryDescriptor() { return const StylerFactoryDescriptor( name: 'animate', - signature: 'animate(AnimationConfig value)', - invocation: 'animate(value)', + signature: 'animate(AnimationConfig value, {AnimationConfig? reverse})', + invocation: 'animate(value, reverse: reverse)', ); } diff --git a/packages/mix_generator/test/core/builders/styler_mixin_builder_test.dart b/packages/mix_generator/test/core/builders/styler_mixin_builder_test.dart index 15a6e919dd..1651b25ede 100644 --- a/packages/mix_generator/test/core/builders/styler_mixin_builder_test.dart +++ b/packages/mix_generator/test/core/builders/styler_mixin_builder_test.dart @@ -290,8 +290,19 @@ void main() { ); final code = builder.build(); - expect(code, contains('BoxStyler animate(AnimationConfig value)')); - expect(code, contains('return merge(BoxStyler(animation: value))')); + expect( + code, + contains( + 'BoxStyler animate(AnimationConfig value, {AnimationConfig? reverse})', + ), + ); + expect( + code, + contains( + 'ReversibleAnimationConfig(forward: value, reverse: reverse)', + ), + ); + expect(code, contains('return merge(BoxStyler(animation: config))')); }); test('generates variants method', () { diff --git a/packages/mix_generator/test/integration/spec_styler_generator_smoke_test.dart b/packages/mix_generator/test/integration/spec_styler_generator_smoke_test.dart index f3879d2ba6..86b919c0d7 100644 --- a/packages/mix_generator/test/integration/spec_styler_generator_smoke_test.dart +++ b/packages/mix_generator/test/integration/spec_styler_generator_smoke_test.dart @@ -60,6 +60,11 @@ const _mixStub = ''' } class AnimationConfig {} + class ReversibleAnimationConfig extends AnimationConfig { + final AnimationConfig forward; + final AnimationConfig reverse; + const ReversibleAnimationConfig({required this.forward, required this.reverse}); + } class WidgetModifierConfig { static WidgetModifierConfig defaultTextStyler(Object value) => WidgetModifierConfig(); Object? resolve(Object context) => null; @@ -943,7 +948,9 @@ void main() { contains('TrivialStyler({'), contains('EdgeInsetsGeometryMix? padding'), contains('Prop.maybeMix(padding)'), - contains('TrivialStyler animate(AnimationConfig value)'), + contains( + 'TrivialStyler animate(AnimationConfig value, {AnimationConfig? reverse})', + ), contains( 'TrivialStyler variants(List> value)', ), @@ -1300,7 +1307,7 @@ void main() { ), contains('factory BoxStyler.translate(double x, double y'), contains('factory BoxStyler.textStyle(TextStyler value)'), - contains('factory BoxStyler.animate(AnimationConfig value)'), + contains('factory BoxStyler.animate('), ]), ), }, @@ -1386,9 +1393,7 @@ void main() { isNot( contains('factory TextStyler.semanticsLabel(String value)'), ), - isNot( - contains('factory TextStyler.animate(AnimationConfig value)'), - ), + isNot(contains('factory TextStyler.animate(')), ]), ), }, @@ -1423,9 +1428,7 @@ void main() { isNot( contains('factory IconStyler.semanticsLabel(String value)'), ), - isNot( - contains('factory IconStyler.animate(AnimationConfig value)'), - ), + isNot(contains('factory IconStyler.animate(')), ]), ), }, @@ -1471,7 +1474,7 @@ void main() { 'mix|lib/box_spec.g.dart': decodedMatches( allOf([ contains('factory BoxStyler.clipBehavior(Clip value)'), - contains('factory BoxStyler.animate(AnimationConfig value)'), + contains('factory BoxStyler.animate('), isNot(contains('factory BoxStyler.color(Color value)')), isNot(contains('factory BoxStyler.width(double value)')), isNot(contains('factory BoxStyler.scale(double scale')),