Add reverse animation config for style exit transitions
- Dominant language
- Dart
- Stars
- 800
- Forks
- 49
- Avg merge
- 3h 39m
- Merged PRs (30d)
- 22
Description
## Summary
Add support for declaring an exit animation alongside the normal animation config:
```dart
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:
```dart
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:
```dart
.animate(animation)
```
Add an optional reverse config:
```dart
.animate(animation, reverse: reverseAnimation)
```
The generated static factories should support the same parameter:
```dart
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:
```dart
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:
```dart
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];
}
```
Update generated `animate` methods to wrap only when `reverse` is provided:
```dart
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:
```dart
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:
```bash
cd packages/mix
flutter test test/src/core/style_builder_hover_test.dart
flutter test test/src/animation/animation_config_test.dart
```
Generator tests:
```bash
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:
```bash
melos run gen:build && melos run ci && melos run analyze
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.