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.
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.
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,
),
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With