conceptadev / conceptadev/mix

Add reverse animation config for style exit transitions

Open
#929 0 comments 0 reactions 0 assignees View on GitHub
enhancement mix
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.