'Flutter - How to have arbitrary number of implicit animations

I've made a widget that takes a list of children, and a List<double> of gaps, which displays the children with the respective gap between them. I've made it so passing a new list of gaps causes the widget to animate from the old gap to the new gaps (changing number of gaps not supported).

What's the best way to handle implicity animating the gaps?

This is the kind of behaviour I'm looking for: example
(source: gfycat.com)



Solution 1:[1]

To avoid unneeded repetition you can move the tween logic to a custom widget.

You can also fuse List<Widget> children with List<double> gaps with a custom Gap widget.

Ultimately you can keep using ListView via separated constructor and use our custom Gap as separators.


Taking all of this into consideration, in the end your Gap widget is simply an AnimatedContainer with a custom height:

class Gap extends StatelessWidget {
  final double gap;

  const Gap(this.gap, {Key key})
      : assert(gap >= .0),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 250),
      curve: Curves.easeOut,
      height: gap,
    );
  }
}

And you can then use it using the following:

ListView.separated(
  itemCount: 42,
  addAutomaticKeepAlives: true,
  itemBuilder: (context, index) {
    return RaisedButton(onPressed: null, child: Text("Foo $index"));
  },
  separatorBuilder: (context, index) {
    return Gap(10.0);
  },
),

The addAutomaticKeppAlives: true here is used to ensure that items leaving then reappearing don't have their animation reset. But it is not a necessity.

Here's a full example with dynamically changing gap size:

enter image description here

class Home extends StatefulWidget {
  @override
  HomeState createState() {
    return new HomeState();
  }
}

class HomeState extends State<Home> {
  final rand = Random.secure();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: <Widget>[
          RaisedButton(
            onPressed: () {
              setState(() {});
            },
            child: Text("Reroll random gaps"),
          ),
          Expanded(
            child: ListView.separated(
              addAutomaticKeepAlives: true,
              itemCount: 42,
              itemBuilder: (context, index) {
                print("Bar");
                return RaisedButton(onPressed: () {}, child: Text("Foo $index"));
              },
              separatorBuilder: (context, index) {
                print("Foo $index");
                return Gap(rand.nextDouble() * 10.0);
              },
            ),
          ),
        ],
      ),
    );
  }
}

Solution 2:[2]

This was my solution, but my code is pretty messy. In particular, I'm not sure having a seperate list for the animations, tweens, controllers and curves (which is what I'm doing now) is the best way to do things. Also doing List<int>.generate(widget.gaps.length, (i) => i).forEach in the build function seems wrong, but the usual for (var i; i<x; i++) doesn't seem very dart-y either.

Is there a better way to handle these two issues?

class GappedList extends StatefulWidget {
  final List<Widget> children;
  final List<double> gaps;
  GappedList({@required this.children, @required this.gaps}) :
    assert(children != null),
    assert(children.length > 0),
    assert(gaps != null),
    assert (gaps.length >= children.length - 1);
  @override
  GappedListState createState() {
    return new GappedListState();
  }
}

class GappedListState extends State<GappedList> with TickerProviderStateMixin{
  List<Animation> _animations = [];
  List<AnimationController> _controllers = [];
  List<CurvedAnimation> _curves = [];
  List<Tween<double>> _tweens;

  @override
  void initState() {
    super.initState();
    _tweens = widget.gaps.map((g) => Tween(
      begin: g ?? 0.0,
      end: g ?? 0.0,
    )).toList();
    _tweens.forEach((t) {
      _controllers.add(AnimationController(
        value: 1.0,
        vsync: this,
        duration: Duration(seconds: 1),
      ));
      _curves.add(CurvedAnimation(parent: _controllers.last, curve: Curves.ease));
      _animations.add(t.animate(_curves.last));
    });
  }
  @override
  void dispose() {
    _controllers.forEach((c) => c.dispose());
    super.dispose();
  }

  @override
  void didUpdateWidget(GappedList oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(oldWidget.gaps.length == widget.gaps.length);
    List<Tween<double>> oldTweens = _tweens;
    List<int>.generate(widget.gaps.length, (i) => i).forEach(
      (i) {
        _tweens[i] = Tween<double>(
          begin: oldTweens[i].evaluate(_curves[i]),
          end: widget.gaps[i] ?? 0.0,
        );
        _animations[i] = _tweens[i].animate(_curves[i]);
        if (_tweens[i].begin != _tweens[i].end) {
          _controllers[i].forward(from: 0.0);
        }
      }
    );
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> list = [];
    List<int>.generate(widget.children.length, (i) => i).forEach(
      (i) {
        list.add(widget.children[i]);
        if (widget.children[i] != widget.children.last) {
          list.add(
            AnimatedBuilder(
              animation: _animations[i],
              builder: (context, _) => ConstrainedBox(
                constraints: BoxConstraints.tightForFinite(
                  height: _animations[i].value,
                ),
              ),
            )
          );
        }
      }
    );
    return ListView(
      primary: true,
      shrinkWrap: true,
      children: list,
    );
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Rémi Rousselet
Solution 2