17

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.

2
  • Please share the code Commented Oct 3, 2021 at 6:42
  • edited and added the code Commented Oct 3, 2021 at 7:01

1 Answer 1

40
+100

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

3 Comments

That is actually PERFECT. thank you.
@MehrzadMohammadi Thank you as well. This is actually a pretty common question, I upvoted yours, and closed another similar question for being duplicated. If this answer continuous to receive attention, I will polish it and release it as a pub library.
@WSBT It was useful to me as well, did you end up publishing it anywhere?

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.