mix icon indicating copy to clipboard operation
mix copied to clipboard

Animations as attribute

Open tilucasoli opened this issue 1 year ago • 3 comments

Package version

1.0.0-beta.1

Flutter version

Flutter 3.16.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 67457e669f (10 days ago) • 2024-01-16 16:22:29 -0800
Engine • revision 6e2ea58a5c
Tools • Dart 3.2.5 • DevTools 2.28.5

Steps to reproduce

I tried to create a animation that changes the background color to another over a specific duration when the user hovers the widget. To do this, I used an AnimatedBox as shown in the code below:

Pressable(
  child: AnimatedBox(
    duration: const Duration(milliseconds: 300),
    style: Style(
      height(50),
      width(50),
      backgroundColor.red(),
      onHover(
        backgroundColor.blue(),
      ),
    ),
  ),
)

Perfect! This code works really well. to make this animation avaliable to other widgets. I developed an attribute to handle it using WidgetDecorator, as shown below::

class ColorTransitionOnHoverDecorator
    extends WidgetDecorator<ColorTransitionOnHoverDecorator> {
  final Color initialColor;
  final Color finalColor;
  final Duration duration;

  const ColorTransitionOnHoverDecorator(
    this.initialColor,
    this.finalColor, {
    required this.duration,
    super.key,
  });

  @override
  ColorTransitionOnHoverDecorator lerp(VisibilityDecorator? other, double t) {
    return ColorTransitionOnHoverDecorator(initialColor, finalColor,
        duration: Duration.zero);
  }

  @override
  get props => [initialColor];

  @override
  Widget build(mix, child) => AnimatedBox(
        duration: duration,
        style: Style(
          box.color(initialColor),
          onHover(
            box.color(finalColor),
          ),
        ),
        child: child,
      );
}

class ColorTransitionOnHoverUtility<T extends StyleAttribute>
    extends MixUtility<T, ColorTransitionOnHoverDecorator> {
  const ColorTransitionOnHoverUtility(super.builder);

  T call(
    Color initialColor,
    Color finalColor, {
    required Duration duration,
    Key? key,
  }) =>
      builder(ColorTransitionOnHoverDecorator(
        initialColor,
        finalColor,
        duration: duration,
        key: key,
      ));
}

final colorTransitionOnHover = ColorTransitionOnHoverUtility((d) => d);

Therefore, I replace the AnimatedBox with the new attribute colorTransitionOnHover:


Pressable(
  child: Box(
    style: Style(
      height(50),
      width(50),
      colorTransitionOnHover(
        Colors.red,
        Colors.blue,
        duration: const Duration(milliseconds: 300),
      ),
    ),
  ),
)

It's also work well! However, when I applied the same attribute colorTransitionOnHover to an HBox, it didn't work. The HBox didn't receive any Flex attributes.

Pressable(
  child: HBox(
    style: Style(
      flex.mainAxisSize.min(),
      flex.crossAxisAlignment.center(),
      flex.mainAxisAlignment.end(),
      padding(10),
      colorTransitionOnHover(
        Colors.blueAccent,
        Colors.redAccent,
        duration: const Duration(milliseconds: 300),
      ),
    ),
    children: [
      Box(
        style: Style(
          height(50),
          width(50),
          backgroundColor.black(),
        ),
      ),
      Box(
        style: Style(
          height(50),
          width(50),
          backgroundColor.white(),
        ),
      )
    ],
  ),
);

Expected results

https://github.com/conceptadev/mix/assets/62367544/d8907e4d-3e60-4f7d-bd5b-e05e92e34483

Actual results

https://github.com/conceptadev/mix/assets/62367544/1f6409bc-f0b8-4bc9-bee3-1a955072b11a

Code sample

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: WidgetAnimatedWithAttribute(),
        ),
      ),
    );
  }
}

class WidgetAnimatedWithoutAttribute extends StatelessWidget {
  const WidgetAnimatedWithoutAttribute({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Pressable(
      child: AnimatedBox(
        duration: const Duration(milliseconds: 300),
        style: Style(
          box.color(Colors.blueAccent),
          onHover(
            box.color(Colors.redAccent),
          ),
        ),
        child: HBox(
          style: Style(
            flex.mainAxisSize.min(),
            flex.crossAxisAlignment.center(),
            flex.mainAxisAlignment.end(),
            padding(10),
          ),
          children: [
            Box(
              style: Style(
                height(50),
                width(50),
                backgroundColor.black(),
              ),
            ),
            Box(
              style: Style(
                height(50),
                width(50),
                backgroundColor.white(),
              ),
            )
          ],
          // duration: const Duration(milliseconds: 300),
        ),
      ),
    );
  }
}

class WidgetAnimatedWithAttribute extends StatelessWidget {
  const WidgetAnimatedWithAttribute({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Pressable(
      child: HBox(
        style: Style(
          flex.mainAxisSize.min(),
          flex.crossAxisAlignment.center(),
          flex.mainAxisAlignment.end(),
          padding(10),
          colorTransitionOnHover(
            Colors.blueAccent,
            Colors.redAccent,
            duration: const Duration(milliseconds: 300),
          ),
        ),
        children: [
          Box(
            style: Style(
              height(50),
              width(50),
              backgroundColor.black(),
            ),
          ),
          Box(
            style: Style(
              height(50),
              width(50),
              backgroundColor.white(),
            ),
          )
        ],
        // duration: const Duration(milliseconds: 300),
      ),
    );
  }
}

class ColorTransitionOnHoverDecorator
    extends WidgetDecorator<ColorTransitionOnHoverDecorator> {
  final Color initialColor;
  final Color finalColor;
  final Duration duration;

  const ColorTransitionOnHoverDecorator(
    this.initialColor,
    this.finalColor, {
    required this.duration,
    super.key,
  });

  @override
  ColorTransitionOnHoverDecorator lerp(VisibilityDecorator? other, double t) {
    return ColorTransitionOnHoverDecorator(initialColor, finalColor,
        duration: Duration.zero);
  }

  @override
  get props => [initialColor];

  @override
  Widget build(mix, child) => AnimatedBox(
        duration: duration,
        style: Style(
          box.color(initialColor),
          onHover(
            box.color(finalColor),
          ),
        ),
        child: child,
      );
}

class ColorTransitionOnHoverUtility<T extends StyleAttribute>
    extends MixUtility<T, ColorTransitionOnHoverDecorator> {
  const ColorTransitionOnHoverUtility(super.builder);

  T call(
    Color initialColor,
    Color finalColor, {
    required Duration duration,
    Key? key,
  }) =>
      builder(ColorTransitionOnHoverDecorator(
        initialColor,
        finalColor,
        duration: duration,
        key: key,
      ));
}

final colorTransitionOnHover = ColorTransitionOnHoverUtility((d) => d);

tilucasoli avatar Jan 26 '24 17:01 tilucasoli

@tilucasoli It seems that MixedFlex widget is unable to retrieve MixData from the context due to it being overridden by AnimatedBox.

As a solution, we can pass MixData explicitly to both MixedFlex and MixedBox by following this link https://github.com/conceptadev/mix/blob/main/lib/src/specs/flex/flex_widget.dart#L191.

Additionally, to have more control over mix parameter for the Mixed widgets, we can make it a required parameter instead of grabbing it from the context.

leoafarias avatar Jan 26 '24 18:01 leoafarias

Mix has been updated a lot and this issue specifically is outdated. Nowadays, you can create animation with a new kind of Style, the AnimatedStyle, which can be animated. Reproducing the same classes in this issue with the new API, it looks like:

Without animation

class WidgetAnimatedWithoutAttribute extends StatelessWidget {
  const WidgetAnimatedWithoutAttribute({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return PressableBox(
      onPress: () {},
      style: Style(
        $box.color(Colors.blueAccent),
        $on.hover(
          $box.color(Colors.redAccent),
        ),
      ),
      child: HBox(
        style: Style(
          $flex.mainAxisSize.min(),
          $flex.crossAxisAlignment.center(),
          $flex.mainAxisAlignment.end(),
          $box.padding(10),
        ),
        children: [
          Box(
            style: Style(
              $box.height(50),
              $box.width(50),
              $box.color.black(),
            ),
          ),
          Box(
            style: Style(
              $box.height(50),
              $box.width(50),
              $box.color.white(),
            ),
          )
        ],
      ),
    );
  }
}

With animation

class WidgetAnimatedWithAttribute extends StatelessWidget {
  const WidgetAnimatedWithAttribute({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return PressableBox(
      onPress: () {},
      style: Style(
        $box.color(Colors.blue),
        $on.hover(
          $box.color(Colors.red),
        ),
      ).animate(
        duration: const Duration(milliseconds: 300),
      ),
      child: HBox(
        style: Style(
          $flex.mainAxisSize.min(),
          $flex.crossAxisAlignment.center(),
          $flex.mainAxisAlignment.end(),
          $box.padding(10),
        ),
        children: [
          Box(
            style: Style(
              $box.height(50),
              $box.width(50),
              $box.color(Colors.black),
            ),
          ),
          Box(
            style: Style(
              $box.height(50),
              $box.width(50),
              $box.color.white(),
            ),
          )
        ],
      ),
    );
  }
}

tilucasoli avatar May 23 '24 14:05 tilucasoli

An interesting thing about this issue is that we need to create a decorator to create a reusable animation, and in the current version we can merge Style and in this way, we can reuse styles. Thinking about reusing standard attributes in other components and styles, how about we create an easy way to group attributes and make them reusable?

I was thinking of something like this

Attribute colorTransitionOnHover(
  Color initialColor,
  Color finalColor,
) {
  return group(
    $box.color(initialColor),
    $on.hover(
      $box.color(finalColor),
    ),
  );
}

tilucasoli avatar May 23 '24 14:05 tilucasoli