2

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)

4
  • 1
    Stackoverflow is not about code-review. See meta.stackexchange.com/questions/199680/… for more infos. You might want instead to make a question like "How to do X ?" and then add your own solution as answer. And see what others offer Commented Aug 21, 2018 at 9:39
  • Okay I'll change it a bit, thanks Commented Aug 21, 2018 at 9:42
  • Nice. Can you also add a gif of the desired visual behavior on the question ? :) Commented Aug 21, 2018 at 9:48
  • @RémiRousselet done! Commented Aug 21, 2018 at 10:02

2 Answers 2

3

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);
              },
            ),
          ),
        ],
      ),
    );
  }
}
Sign up to request clarification or add additional context in comments.

2 Comments

This is great, thanks! There's always an easy widget like that, trick is knowing what!
I like to think that in flutter, if you need more than 100 lines to do something then you did it wrong :p
1

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,
    );
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.