flutter_animate icon indicating copy to clipboard operation
flutter_animate copied to clipboard

Add An Equivalent to `AnimatedSwitcher`

Open caseycrogers opened this issue 1 year ago • 8 comments
trafficstars

One of the things I love the most about flutter_animate is that I can use the extensions to visually separate (in my code) UI elements from the animation effects applied to them. This makes my code MASSIVELY more readable.

The one place where I've struggled is I have a bunch of AnimatedSwitchers and I'm not quite sure how to migrate them. Here's one such example:

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      duration: kFlipDuration,
      transitionBuilder: flipTransitionBuilder,
      child: isFlipped ? secondChild : firstChild,
      switchInCurve: Curves.easeInBack,
      switchOutCurve: Curves.easeInBack.flipped,
    );
  }

  Widget flipTransitionBuilder<T>(
    Widget child,
    Animation<double> animation,
  ) { ... }
}

I think what I've done above could be achieved via something like toggleEffect + flipEffect, but it feels a bit messy so thus far I've just stuck with AnimatedSwitcher while I migrated all my other animations to flutter_animate. So, this isn't the highest pri, but here's what I'd like (and I may build a prototype myself): a SwitchEffect that is as close to AnimatedSwitcher as possible.

return SwitchEffect(
  child: isFlipped ? secondChild : firstChild,
).animate().flipEffect( ... ).toggleEffect( ... ); // The effects are only triggered when the child changes.

This feels very close to just return ToggleEffect( ... ) but as far as I can tell toggle effect is dependent on the animation, not on an external variable (like the widget child).

I also just may be missing something in flutter_animate that already does this, if I have please let me know as I'd love to migrate all my vanilla fluttter animation logic into flutter_animate!

caseycrogers avatar Dec 07 '23 00:12 caseycrogers

I think you could probably get there with swap, but it might be easier to just write a simple widget that mimics the capabilities of AnimatedSwitcher, but lets you use Animate for the transition?

AnimateSwitcher(
  child: isFlipped ? second : first,
  outEffects: EffectList().fadeOut(),
  inEffects: EffectList().fadeIn(),
)

Where it would run the outEffects on the outgoing child, then the inEffects on the incoming child. Maybe allow for an overlap param, so they can be layered over each other and run simultaneously? Or even an enum for specifying no overlap, layering incoming on top, or outgoing on top.

Feel free to take a stab at this as a PR. If not, I might look at it when I have a bit of time.

gskinner avatar Jan 30 '24 17:01 gskinner

I had no success in building this widget. Currently the first in effect works as well as the second inEffect for the contemplary widget. The out effects are not working and consecutive effects are also not working.

I played around with different possibilities even with FutureBuilder instead of swap() but with no major success so far.

return SwitchEffect(
  child: pos == 0 ? _icon(Icons.light_mode) : _icon(Icons.dark_mode),
  inEffects: EffectList().fadeIn(duration: 2.seconds),
  outEffects: EffectList().fadeOut(duration: 2.seconds),
);

and the widget itself so far:

import 'package:flutter/cupertino.dart';
import 'package:flutter_animate/flutter_animate.dart';

class SwitchEffect extends StatefulWidget {
  final Widget child;

  final List<Effect>? inEffects;

  final List<Effect>? outEffects;

  SwitchEffect({required this.child, this.inEffects, this.outEffects});

  @override
  State<StatefulWidget> createState() {
    return _SwitchEffectState();
  }
}

////////////////////////////////////////////////////////////////////////////////

class _SwitchEffectState extends State {
  @override
  SwitchEffect get widget => super.widget as SwitchEffect;

  Widget? _lastChild;

  List<Effect>? _lastOutEffects;

  @override
  Widget build(BuildContext context) {
    if (_lastChild != null) {
      return _lastChild!
          .animate()
          .addEffects(_lastOutEffects ?? EffectList().fadeOut())
          .then()
          .swap(
              delay: 0.seconds,
              duration: 0.seconds,
              builder: (BuildContext context, Widget? _) {
                _lastChild = null;
                _lastOutEffects = null;
                return widget.child;
              })
          .then()
          .addEffects(widget.inEffects ?? EffectList().fadeIn());
    }
    return widget.child
        .animate()
        .addEffects(widget.inEffects ?? EffectList().fadeIn());
  }

  @override
  void didUpdateWidget(covariant SwitchEffect oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.child != widget.child) {
      _lastChild = oldWidget.child;
      _lastOutEffects = oldWidget.outEffects;
    }
  }
}

mikes222 avatar Jan 30 '24 21:01 mikes222

As a bit of quick guidance, the widget you swap in with swap completely replaces the prior target and it's Animate instance (well, not exactly, but it's the easiest way to conceptualize it), so you want something that looks more like this:

Animate(child: oldChild, effects: outEffects).then().swap(
  builder: (_, __) => Animate(child: newChild, effects: inEffects), 
)

Haven't tested that, but I think it should work.

gskinner avatar Jan 30 '24 23:01 gskinner

Unfortunately it is not fully working that way. With the snippet below the in and out effect for the first icon works as well as the in-effect for the alternate icon. From then on no effect is applied anymore. It seems like the animations are somewhat consumed and cannot be used anymore.

The swap also needs to have the duration and delay setting at default. When setting 0 the animation is not working (better to say not visible) anymore.

@override
Widget build(BuildContext context) {
  if (_lastChild != null) {
    return Animate(
      child: _lastChild!,
      effects: _lastOutEffects ?? EffectList().fadeOut(),
      autoPlay: true,
    ).then().swap(
        //duration: 0.seconds,
        //delay: 0.seconds,
        builder: (_, __) => Animate(
              child: widget.child,
              effects: widget.inEffects ?? EffectList().fadeIn(),
              autoPlay: true,
            ));
  }
  return Animate(
    child: widget.child,
    effects: widget.inEffects ?? EffectList().fadeIn(),
    autoPlay: true,
  );
}

mikes222 avatar Jan 31 '24 19:01 mikes222

Might be worth trying to add a key to the Animate instances. There's a decent chance that Flutter is just matching them as the same instance (same type, plus same child type), so it doesn't trigger playing.

gskinner avatar Jan 31 '24 21:01 gskinner

first tests were successful. Will do more tests tomorrow...

mikes222 avatar Jan 31 '24 22:01 mikes222

I also need this feature to have what AnimatedSwitcher is doing. Wondering when will the PR from @mikes222 will be merged.

lei-cao avatar Mar 05 '24 11:03 lei-cao