Move an item from one list to another with animation in Flutter

Issue

I have two vertical lists, one on the left side and the other one on the right, let’s call them "Selected List" and "Unselected List".
I want the items in Unselected List to Animate from left side to the right side of the screen and add to Selected List.
the other items should fill the empty space in Unselected List and items in Selected List should free up the space for new item.
Here’s the Ui

My Code:

class AddToFave extends StatefulWidget {
  const AddToFave({Key? key}) : super(key: key);

  @override
  _AddToFaveState createState() => _AddToFaveState();
}

class _AddToFaveState extends State<AddToFave> {
  List<String> unselected = [ '1','2','3','4','5','6','7','8','9','10'];
  List<String> selected = [];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Container(
              width: MediaQuery.of(context).size.width / 5,
              height: MediaQuery.of(context).size.height,
              child: ListView.builder(
                  itemCount: selected.length,
                  itemBuilder: (context, index) {
                    return InkWell(
                      onTap: () {
                        unselected.add(selected[index]);
                        selected.removeAt(index);
                        setState(() {});
                      },
                      child: Container(
                        width: MediaQuery.of(context).size.width / 5,
                        height: MediaQuery.of(context).size.width / 5,
                        decoration: BoxDecoration(
                            color: Colors.black,
                            borderRadius: BorderRadius.circular(
                                MediaQuery.of(context).size.width / 5)),
                        child: Center(
                            child: Text(
                          selected[index],
                          style: TextStyle(color: Colors.white),
                        )),
                      ),
                    );
                  }),
            ),
            Container(
              width: MediaQuery.of(context).size.width / 5,
              height: MediaQuery.of(context).size.height,
              child: ListView.builder(
                  itemCount: unselected.length,
                  itemBuilder: (context, index) {
                    return InkWell(
                      onTap: () {
                        selected.add(unselected[index]);
                        unselected.removeAt(index);
                        setState(() {});
                      },
                      child: Container(
                        width: MediaQuery.of(context).size.width / 5,
                        height: MediaQuery.of(context).size.width / 5,
                        decoration: BoxDecoration(
                            color: Colors.black,
                            borderRadius: BorderRadius.circular(
                                MediaQuery.of(context).size.width / 5)),
                        child: Center(
                            child: Text(
                          unselected[index],
                          style: TextStyle(color: Colors.white),
                        )),
                      ),
                    );
                  }),
            ),
          ],
        ),
      ),
    );
  }
}

Thank you in advance.

Solution

This task can be broken into 2 parts.

First, use an AnimatedList instead of a regular ListView, so that when an item is removed, you can control its "exit animation" and shrink its size, thus making other items slowly move upwards to fill in its spot.

Secondly, while the item is being removed from the first list, make an OverlayEntry and animate its position, to create an illusion of the item flying. Once the flying is finished, we can remove the overlay and insert the item in the actual destination list.

demo gif

Full source code for you to use, as a starting point:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TwoAnimatedListDemo(),
    );
  }
}

class TwoAnimatedListDemo extends StatefulWidget {
  const TwoAnimatedListDemo({Key? key}) : super(key: key);

  @override
  _TwoAnimatedListDemoState createState() => _TwoAnimatedListDemoState();
}

class _TwoAnimatedListDemoState extends State<TwoAnimatedListDemo> {
  final List<String> _unselected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
  final List<String> _selected = [];

  final _unselectedListKey = GlobalKey<AnimatedListState>();
  final _selectedListKey = GlobalKey<AnimatedListState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Two Animated List Demo'),
      ),
      body: Row(
        children: [
          SizedBox(
            width: 56,
            child: AnimatedList(
              key: _unselectedListKey,
              initialItemCount: _unselected.length,
              itemBuilder: (context, index, animation) {
                return InkWell(
                  onTap: () => _moveItem(
                    fromIndex: index,
                    fromList: _unselected,
                    fromKey: _unselectedListKey,
                    toList: _selected,
                    toKey: _selectedListKey,
                  ),
                  child: Item(text: _unselected[index]),
                );
              },
            ),
          ),
          Spacer(),
          SizedBox(
            width: 56,
            child: AnimatedList(
              key: _selectedListKey,
              initialItemCount: _selected.length,
              itemBuilder: (context, index, animation) {
                return InkWell(
                  onTap: () => _moveItem(
                    fromIndex: index,
                    fromList: _selected,
                    fromKey: _selectedListKey,
                    toList: _unselected,
                    toKey: _unselectedListKey,
                  ),
                  child: Item(text: _selected[index]),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  int _flyingCount = 0;

  _moveItem({
    required int fromIndex,
    required List fromList,
    required GlobalKey<AnimatedListState> fromKey,
    required List toList,
    required GlobalKey<AnimatedListState> toKey,
    Duration duration = const Duration(milliseconds: 300),
  }) {
    final globalKey = GlobalKey();
    final item = fromList.removeAt(fromIndex);
    fromKey.currentState!.removeItem(
      fromIndex,
      (context, animation) {
        return SizeTransition(
          sizeFactor: animation,
          child: Opacity(
            key: globalKey,
            opacity: 0.0,
            child: Item(text: item),
          ),
        );
      },
      duration: duration,
    );
    _flyingCount++;

    WidgetsBinding.instance!.addPostFrameCallback((timeStamp) async {
      // Find the starting position of the moving item, which is exactly the
      // gap its leaving behind, in the original list.
      final box1 = globalKey.currentContext!.findRenderObject() as RenderBox;
      final pos1 = box1.localToGlobal(Offset.zero);
      // Find the destination position of the moving item, which is at the
      // end of the destination list.
      final box2 = toKey.currentContext!.findRenderObject() as RenderBox;
      final box2height = box1.size.height * (toList.length + _flyingCount - 1);
      final pos2 = box2.localToGlobal(Offset(0, box2height));
      // Insert an overlay to "fly over" the item between two lists.
      final entry = OverlayEntry(builder: (BuildContext context) {
        return TweenAnimationBuilder(
          tween: Tween<Offset>(begin: pos1, end: pos2),
          duration: duration,
          builder: (_, Offset value, child) {
            return Positioned(
              left: value.dx,
              top: value.dy,
              child: Item(text: item),
            );
          },
        );
      });

      Overlay.of(context)!.insert(entry);
      await Future.delayed(duration);
      entry.remove();
      toList.add(item);
      toKey.currentState!.insertItem(toList.length - 1);
      _flyingCount--;
    });
  }
}

class Item extends StatelessWidget {
  final String text;

  const Item({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: CircleAvatar(
        child: Text(text),
        radius: 24,
      ),
    );
  }
}

Answered By – user1032613

Answer Checked By – Dawn Plyler (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.