Skip to content

Add reverse animation config for style exit transitions #929

Description

@leoafarias

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:

.animate(animation)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions