Animating item removal in a grid in Flutter

Viewed 4124

I have a grid of boxes using the Wrap widget (I did not use the GridView as this requires you stating how many items you need in each row ahead of time).

I want to remove the item when they are clicked, and have all the other items animate to their new position like this: https://vestride.github.io/Shuffle/adding-removing (click on the boxes to see what I mean).

Here is my code so far without any animation:

class Boxes extends StatefulWidget {
  @override
  _BoxesState createState() => _BoxesState();
}

class _BoxesState extends State<Boxes> {
  var items = [
    {'id': '25', 'name': 'A',},
    {'id': '19', 'name': 'B',},
    {'id': '35', 'name': 'C',},
    {'id': '20', 'name': 'D',},
    {'id': '958', 'name': 'E',},
    {'id': '1278', 'name': 'F',},
    {'id': '500', 'name': 'G',},
  ];

  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      child: Wrap(
        alignment: WrapAlignment.center,
        children: [
          for (final item in items)
            Box(
              key: Key(item['id']),
              name: item['name'],
              onDelete: () {
                setState(() {
                  items.remove(item);
                });
              },
            )
        ],
      ),
    );
  }
}

class Box extends StatelessWidget {
  String name;
  Function onDelete;

  Box({this.name, this.onDelete, Key key}):
    super(key: key);

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onDelete,
      child: Container(
        color: Colors.lightBlue,
        width: 90,
        height: 90,
        margin: EdgeInsets.all(8),
        child: Center(
          child: Text(name),
        )
      ),
    );
  }
}

The closest built-in widget I could find is AnimatedList, but that does not work with a grid. I also tried animating the width of the deleted box to 0, which did not work as the other boxes just jump into position instead of animating to the new position.

How would I go about doing this?

3 Answers

Simple but effective:

  1. Create an async Function that handles deletes and declares wait delays
  2. Call the delete function to handle independent deletes on each element
  3. Animate using an AnimatedContainer and an AnimatedOpacity based on the state

Here is a working example using Chips as widgets:

Note: I'll leave for you to decide the max width of the Custom Widget "Box" you will use and if needed calculate its dynamic width depending on its content

//declare an empty list that handles items to be deleted
List<String> deleteItems = [];

//define async Function that handles progressive deletes from the list
  void deleteItem(String id) async {
    setState(() {
      deleteItems.add(id);
    });
    Future.delayed(Duration(milliseconds: 250)).whenComplete(() {
      setState(() {
        deleteItems.removeWhere((i) => i == id);
        items.removeWhere((i) => i["id"] == id);
      });
    });
  }

//Widget to be used to animated the List/Wrap
  return SizedBox.expand(
      child: Wrap(
    alignment: WrapAlignment.center,
    children: List.generate(items.length, (index) {
      var item = items[index];
      bool isMarkedForDelete =
          deleteItems.where((i) => i == item["id"]).isNotEmpty;
      return AnimatedContainer(
          key: ObjectKey(item),
          duration: Duration(milliseconds: 250),
          //alignment: Alignment.centerLeft,
          width: isMarkedForDelete ? 0 : 60, //change depending on font size or content
          child: AnimatedOpacity(
              duration: Duration(milliseconds: 250),
              opacity: isMarkedForDelete ? 0 : 1,
              child: Chip(
                  label: Text("${item["name"]}"),
                  backgroundColor:
                      isMarkedForDelete ? Colors.red : Colors.blue,
                  deleteIcon: Icon(Icons.close),
                  onDeleted: () {
                    if(!isMarkedForDelete) deleteItem(item["id"] ?? "");
                  })));
    }),
  ));

Geetings.

You could give a try to use AnimatedContainer

I basically change the width from 100 to 0, when the tap event occurs.

I also change the list items a little.

AnimatedContainer detects that the property width changed and fires the animation.

Please try something like this (is not the same as your reference)

I commented the items.remove(item), you need to find a way to really remove the item once the animation is finished (maybe with a timer or future).

  List<Map<String,dynamic>> items = [
     {'id':'11','name':'A','width':100},
     {'id':'12','name':'B','width':100},
     {'id':'13','name':'C','width':100},
     {'id':'14','name':'D','width':100},
     {'id':'15','name':'E','width':100},
     {'id':'16','name':'F','width':100},
     {'id':'17','name':'G','width':100},
  ];

  Widget build(BuildContext context) {
    return Scaffold(body: Container(
      width: double.infinity,
      height: double.infinity,
      child: Wrap(
        alignment: WrapAlignment.center,
        children: [
          for (final item in items)
            AnimatedContainer(
              width: double.parse(item['width'].toString()),
              duration: Duration(milliseconds: 600),
              curve: Curves.easeInBack,
              child: Box(
                key: Key(item['id']),
                name: item['name'],
                onDelete: () {
                  setState(() {
                    //items.remove(item);
                    item['name'] = "";
                    item['width'] = 0;
                  });
                },
              ),
            )
        ],
      ),
    ));
  }

Flutter video --> https://www.youtube.com/watch?v=yI-8QHpGIP4

Related