Summary
Add support for declaring an exit animation alongside the normal animation config:
BoxStyler()
.onHovered(
BoxStyler()
.scale(1.02)
.animate(hoverIn, reverse: hoverOut),
);
When the hover style becomes active, Mix should use hoverIn. When the hover style is no longer active, Mix should use hoverOut. If no reverse config is declared, Mix should keep the current behavior and use the new target style's animation config.
Problem
Today animation config is selected from the resolved target StyleSpec.animation. This works for simple hover-in/hover-out cases if the base style owns the exit animation and the hover style owns the enter animation:
BoxStyler()
.animate(exitAnimation)
.onHovered(BoxStyler().animate(enterAnimation));
That is functional, but it splits a single interaction's enter/exit timing across two styles. It would be more ergonomic for a variant style to own both its enter and exit transition configs.
Proposed API
Keep existing calls source-compatible:
Add an optional reverse config:
.animate(animation, reverse: reverseAnimation)
The generated static factories should support the same parameter:
BoxStyler.animate(animation, reverse: reverseAnimation)
Proposed Semantics
- Use the target style's forward animation when entering that style.
- Use the old style's reverse animation when leaving that style.
- Only use the old reverse animation when the old and new animation metadata are distinct.
- If the old and new specs carry the same reversible config, use the forward config. This prevents an inherited base animation from being mistaken for a hover exit animation.
reverse is an exit transition config. It does not mean calling AnimationController.reverse().
The recommended hover usage is:
BoxStyler()
.animate(baseAnimation)
.onHovered(
BoxStyler()
.scale(1.02)
.animate(hoverIn, reverse: hoverOut),
);
Implementation Notes
Use an AnimationConfig wrapper rather than adding a new generated styler field:
final class ReversibleAnimationConfig extends AnimationConfig with Equatable {
final AnimationConfig forward;
final AnimationConfig reverse;
const ReversibleAnimationConfig({
required this.forward,
required this.reverse,
});
@override
List<Object?> get props => [forward, reverse];
}
Update generated animate methods to wrap only when reverse is provided:
T animate(AnimationConfig value, {AnimationConfig? reverse}) {
final config = reverse == null
? value
: ReversibleAnimationConfig(forward: value, reverse: reverse);
return merge(T(animation: config));
}
In StyleAnimationBuilder, select the effective transition config from old/new metadata:
AnimationConfig? forwardOf(AnimationConfig? config) {
return switch (config) {
ReversibleAnimationConfig(:final forward) => forward,
_ => config,
};
}
AnimationConfig? reverseOf(AnimationConfig? config) {
return switch (config) {
ReversibleAnimationConfig(:final reverse) => reverse,
_ => null,
};
}
AnimationConfig? transitionConfig(
AnimationConfig? oldConfig,
AnimationConfig? newConfig,
) {
if (oldConfig != newConfig) {
final reverse = reverseOf(oldConfig);
if (reverse != null) return reverse;
}
return forwardOf(newConfig);
}
Driver reuse must compare the active driver kind against the selected transition config kind. Do not compare only oldWidget.spec.animation.runtimeType and widget.spec.animation.runtimeType, because a reverse transition can leave the current driver using a different kind from the current spec's forward config.
Files Likely Affected
packages/mix/lib/src/animation/animation_config.dart
packages/mix/lib/src/animation/style_animation_builder.dart
packages/mix/lib/src/style/mixins/animation_style_mixin.dart
packages/mix_generator/lib/src/core/builders/styler_mixin_builder.dart
packages/mix_generator/lib/src/core/curated/styler_surface_metadata.dart
- Generated
*_spec.g.dart files after melos run gen:build
- Generator tests that assert the emitted
animate(AnimationConfig value) signature
- Widget tests around hover animation config selection
Acceptance Criteria
- Existing
.animate(config) calls continue to compile.
- Generated stylers support
.animate(config, reverse: reverseConfig).
- Static styler factories support
Styler.animate(config, reverse: reverseConfig).
- Hover enter uses the hover style's forward config.
- Hover exit uses the hover style's reverse config when present.
- Hover exit falls back to the target/base config when no reverse config is present.
- A base reversible animation inherited by a hover style does not cause hover enter to use reverse.
- Forward and reverse configs may use different driver kinds without type errors.
Test Plan
Focused package tests:
cd packages/mix
flutter test test/src/core/style_builder_hover_test.dart
flutter test test/src/animation/animation_config_test.dart
Generator tests:
cd packages/mix_generator
dart test test/core/builders/styler_mixin_builder_test.dart
dart test test/integration/spec_styler_generator_smoke_test.dart
Full verification:
melos run gen:build && melos run ci && melos run analyze
Summary
Add support for declaring an exit animation alongside the normal animation config:
When the hover style becomes active, Mix should use
hoverIn. When the hover style is no longer active, Mix should usehoverOut. If no reverse config is declared, Mix should keep the current behavior and use the new target style's animation config.Problem
Today animation config is selected from the resolved target
StyleSpec.animation. This works for simple hover-in/hover-out cases if the base style owns the exit animation and the hover style owns the enter animation:That is functional, but it splits a single interaction's enter/exit timing across two styles. It would be more ergonomic for a variant style to own both its enter and exit transition configs.
Proposed API
Keep existing calls source-compatible:
.animate(animation)Add an optional reverse config:
The generated static factories should support the same parameter:
Proposed Semantics
reverseis an exit transition config. It does not mean callingAnimationController.reverse().The recommended hover usage is:
Implementation Notes
Use an
AnimationConfigwrapper rather than adding a new generated styler field:Update generated
animatemethods to wrap only whenreverseis provided:In
StyleAnimationBuilder, select the effective transition config from old/new metadata:Driver reuse must compare the active driver kind against the selected transition config kind. Do not compare only
oldWidget.spec.animation.runtimeTypeandwidget.spec.animation.runtimeType, because a reverse transition can leave the current driver using a different kind from the current spec's forward config.Files Likely Affected
packages/mix/lib/src/animation/animation_config.dartpackages/mix/lib/src/animation/style_animation_builder.dartpackages/mix/lib/src/style/mixins/animation_style_mixin.dartpackages/mix_generator/lib/src/core/builders/styler_mixin_builder.dartpackages/mix_generator/lib/src/core/curated/styler_surface_metadata.dart*_spec.g.dartfiles aftermelos run gen:buildanimate(AnimationConfig value)signatureAcceptance Criteria
.animate(config)calls continue to compile..animate(config, reverse: reverseConfig).Styler.animate(config, reverse: reverseConfig).Test Plan
Focused package tests:
Generator tests:
Full verification: