· 5 years ago · Oct 13, 2020, 12:36 PM
1import 'package:flutter/foundation.dart';
2import 'package:flutter/material.dart';
3
4// ...
5
6class AnimatedListSample extends StatefulWidget {
7 @override
8 _AnimatedListSampleState createState() => _AnimatedListSampleState();
9}
10
11class _AnimatedListSampleState extends State<AnimatedListSample> {
12 final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
13 ListModel<int> _list;
14 int _selectedItem;
15 int _nextItem; // The next item inserted when the user presses the '+' button.
16
17 @override
18 void initState() {
19 super.initState();
20 _list = ListModel<int>(
21 listKey: _listKey,
22 initialItems: <int>[0, 1, 2],
23 removedItemBuilder: _buildRemovedItem,
24 );
25 _nextItem = 3;
26 }
27
28 // Used to build list items that haven't been removed.
29 Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
30 return CardItem(
31 animation: animation,
32 item: _list[index],
33 selected: _selectedItem == _list[index],
34 onTap: () {
35 setState(() {
36 _selectedItem = _selectedItem == _list[index] ? null : _list[index];
37 });
38 },
39 );
40 }
41
42 // Used to build an item after it has been removed from the list. This
43 // method is needed because a removed item remains visible until its
44 // animation has completed (even though it's gone as far this ListModel is
45 // concerned). The widget will be used by the
46 // [AnimatedListState.removeItem] method's
47 // [AnimatedListRemovedItemBuilder] parameter.
48 Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
49 return CardItem(
50 animation: animation,
51 item: item,
52 selected: false,
53 // No gesture detector here: we don't want removed items to be interactive.
54 );
55 }
56
57 // Insert the "next item" into the list model.
58 void _insert() {
59 final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
60 _list.insert(index, _nextItem++);
61 }
62
63 // Remove the selected item from the list model.
64 void _remove() {
65 if (_selectedItem != null) {
66 _list.removeAt(_list.indexOf(_selectedItem));
67 setState(() {
68 _selectedItem = null;
69 });
70 }
71 }
72
73 @override
74 Widget build(BuildContext context) {
75 return MaterialApp(
76 home: Scaffold(
77 appBar: AppBar(
78 title: const Text('AnimatedList'),
79 actions: <Widget>[
80 IconButton(
81 icon: const Icon(Icons.add_circle),
82 onPressed: _insert,
83 tooltip: 'insert a new item',
84 ),
85 IconButton(
86 icon: const Icon(Icons.remove_circle),
87 onPressed: _remove,
88 tooltip: 'remove the selected item',
89 ),
90 ],
91 ),
92 body: Padding(
93 padding: const EdgeInsets.all(16.0),
94 child: AnimatedList(
95 key: _listKey,
96 initialItemCount: _list.length,
97 itemBuilder: _buildItem,
98 ),
99 ),
100 ),
101 );
102 }
103}
104
105/// Keeps a Dart [List] in sync with an [AnimatedList].
106///
107/// The [insert] and [removeAt] methods apply to both the internal list and
108/// the animated list that belongs to [listKey].
109///
110/// This class only exposes as much of the Dart List API as is needed by the
111/// sample app. More list methods are easily added, however methods that
112/// mutate the list must make the same changes to the animated list in terms
113/// of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
114class ListModel<E> {
115 ListModel({
116 @required this.listKey,
117 @required this.removedItemBuilder,
118 Iterable<E> initialItems,
119 }) : assert(listKey != null),
120 assert(removedItemBuilder != null),
121 _items = List<E>.from(initialItems ?? <E>[]);
122
123 final GlobalKey<AnimatedListState> listKey;
124 final dynamic removedItemBuilder;
125 final List<E> _items;
126
127 AnimatedListState get _animatedList => listKey.currentState;
128
129 void insert(int index, E item) {
130 _items.insert(index, item);
131 _animatedList.insertItem(index);
132 }
133
134 E removeAt(int index) {
135 final E removedItem = _items.removeAt(index);
136 if (removedItem != null) {
137 _animatedList.removeItem(
138 index,
139 (BuildContext context, Animation<double> animation) => removedItemBuilder(removedItem, context, animation),
140 );
141 }
142 return removedItem;
143 }
144
145 int get length => _items.length;
146
147 E operator [](int index) => _items[index];
148
149 int indexOf(E item) => _items.indexOf(item);
150}
151
152/// Displays its integer item as 'item N' on a Card whose color is based on
153/// the item's value.
154///
155/// The text is displayed in bright green if [selected] is
156/// true. This widget's height is based on the [animation] parameter, it
157/// varies from 0 to 128 as the animation varies from 0.0 to 1.0.
158class CardItem extends StatelessWidget {
159 const CardItem({
160 Key key,
161 @required this.animation,
162 this.onTap,
163 @required this.item,
164 this.selected: false
165 }) : assert(animation != null),
166 assert(item != null && item >= 0),
167 assert(selected != null),
168 super(key: key);
169
170 final Animation<double> animation;
171 final VoidCallback onTap;
172 final int item;
173 final bool selected;
174
175 @override
176 Widget build(BuildContext context) {
177 TextStyle textStyle = Theme.of(context).textTheme.headline4;
178 if (selected)
179 textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
180 return Padding(
181 padding: const EdgeInsets.all(2.0),
182 child: SizeTransition(
183 axis: Axis.vertical,
184 sizeFactor: animation,
185 child: GestureDetector(
186 behavior: HitTestBehavior.opaque,
187 onTap: onTap,
188 child: SizedBox(
189 height: 80.0,
190 child: Card(
191 color: Colors.primaries[item % Colors.primaries.length],
192 child: Center(
193 child: Text('Item $item', style: textStyle),
194 ),
195 ),
196 ),
197 ),
198 ),
199 );
200 }
201}
202
203void main() {
204 runApp(AnimatedListSample());
205}