-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathanimated_list.dart
996 lines (943 loc) · 35 KB
/
animated_list.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'basic.dart';
import 'framework.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scroll_view.dart';
import 'sliver.dart';
import 'ticker_provider.dart';
/// Signature for the builder callback used by [AnimatedList].
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
/// Signature for the builder callback used by [AnimatedListState.removeItem].
typedef AnimatedListRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation);
// The default insert/remove animation duration.
const Duration _kDuration = Duration(milliseconds: 300);
// Incoming and outgoing AnimatedList items.
class _ActiveItem implements Comparable<_ActiveItem> {
_ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null;
_ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder);
_ActiveItem.index(this.itemIndex)
: controller = null,
removedItemBuilder = null;
final AnimationController controller;
final AnimatedListRemovedItemBuilder removedItemBuilder;
int itemIndex;
@override
int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
}
/// A scrolling container that animates items when they are inserted or removed.
///
/// This widget's [AnimatedListState] can be used to dynamically insert or
/// remove items. To refer to the [AnimatedListState] either provide a
/// [GlobalKey] or use the static [of] method from an item's input callback.
///
/// This widget is similar to one created by [ListView.builder].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
///
/// {@tool sample --template=freeform}
/// This sample application uses an [AnimatedList] to create an effect when
/// items are removed or added to the list.
///
/// ```dart imports
/// import 'package:flutter/foundation.dart';
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
/// class AnimatedListSample extends StatefulWidget {
/// @override
/// _AnimatedListSampleState createState() => _AnimatedListSampleState();
/// }
///
/// class _AnimatedListSampleState extends State<AnimatedListSample> {
/// final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
/// ListModel<int> _list;
/// int _selectedItem;
/// int _nextItem; // The next item inserted when the user presses the '+' button.
///
/// @override
/// void initState() {
/// super.initState();
/// _list = ListModel<int>(
/// listKey: _listKey,
/// initialItems: <int>[0, 1, 2],
/// removedItemBuilder: _buildRemovedItem,
/// );
/// _nextItem = 3;
/// }
///
/// // Used to build list items that haven't been removed.
/// Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
/// return CardItem(
/// animation: animation,
/// item: _list[index],
/// selected: _selectedItem == _list[index],
/// onTap: () {
/// setState(() {
/// _selectedItem = _selectedItem == _list[index] ? null : _list[index];
/// });
/// },
/// );
/// }
///
/// // Used to build an item after it has been removed from the list. This
/// // method is needed because a removed item remains visible until its
/// // animation has completed (even though it's gone as far this ListModel is
/// // concerned). The widget will be used by the
/// // [AnimatedListState.removeItem] method's
/// // [AnimatedListRemovedItemBuilder] parameter.
/// Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
/// return CardItem(
/// animation: animation,
/// item: item,
/// selected: false,
/// // No gesture detector here: we don't want removed items to be interactive.
/// );
/// }
///
/// // Insert the "next item" into the list model.
/// void _insert() {
/// final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
/// _list.insert(index, _nextItem++);
/// }
///
/// // Remove the selected item from the list model.
/// void _remove() {
/// if (_selectedItem != null) {
/// _list.removeAt(_list.indexOf(_selectedItem));
/// setState(() {
/// _selectedItem = null;
/// });
/// }
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Scaffold(
/// appBar: AppBar(
/// title: const Text('AnimatedList'),
/// actions: <Widget>[
/// IconButton(
/// icon: const Icon(Icons.add_circle),
/// onPressed: _insert,
/// tooltip: 'insert a new item',
/// ),
/// IconButton(
/// icon: const Icon(Icons.remove_circle),
/// onPressed: _remove,
/// tooltip: 'remove the selected item',
/// ),
/// ],
/// ),
/// body: Padding(
/// padding: const EdgeInsets.all(16.0),
/// child: AnimatedList(
/// key: _listKey,
/// initialItemCount: _list.length,
/// itemBuilder: _buildItem,
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
/// /// Keeps a Dart [List] in sync with an [AnimatedList].
/// ///
/// /// The [insert] and [removeAt] methods apply to both the internal list and
/// /// the animated list that belongs to [listKey].
/// ///
/// /// This class only exposes as much of the Dart List API as is needed by the
/// /// sample app. More list methods are easily added, however methods that
/// /// mutate the list must make the same changes to the animated list in terms
/// /// of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
/// class ListModel<E> {
/// ListModel({
/// @required this.listKey,
/// @required this.removedItemBuilder,
/// Iterable<E> initialItems,
/// }) : assert(listKey != null),
/// assert(removedItemBuilder != null),
/// _items = List<E>.from(initialItems ?? <E>[]);
///
/// final GlobalKey<AnimatedListState> listKey;
/// final dynamic removedItemBuilder;
/// final List<E> _items;
///
/// AnimatedListState get _animatedList => listKey.currentState;
///
/// void insert(int index, E item) {
/// _items.insert(index, item);
/// _animatedList.insertItem(index);
/// }
///
/// E removeAt(int index) {
/// final E removedItem = _items.removeAt(index);
/// if (removedItem != null) {
/// _animatedList.removeItem(
/// index,
/// (BuildContext context, Animation<double> animation) => removedItemBuilder(removedItem, context, animation),
/// );
/// }
/// return removedItem;
/// }
///
/// int get length => _items.length;
///
/// E operator [](int index) => _items[index];
///
/// int indexOf(E item) => _items.indexOf(item);
/// }
///
/// /// Displays its integer item as 'item N' on a Card whose color is based on
/// /// the item's value.
/// ///
/// /// The text is displayed in bright green if [selected] is
/// /// true. This widget's height is based on the [animation] parameter, it
/// /// varies from 0 to 128 as the animation varies from 0.0 to 1.0.
/// class CardItem extends StatelessWidget {
/// const CardItem({
/// Key key,
/// @required this.animation,
/// this.onTap,
/// @required this.item,
/// this.selected: false
/// }) : assert(animation != null),
/// assert(item != null && item >= 0),
/// assert(selected != null),
/// super(key: key);
///
/// final Animation<double> animation;
/// final VoidCallback onTap;
/// final int item;
/// final bool selected;
///
/// @override
/// Widget build(BuildContext context) {
/// TextStyle textStyle = Theme.of(context).textTheme.headline4;
/// if (selected)
/// textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
/// return Padding(
/// padding: const EdgeInsets.all(2.0),
/// child: SizeTransition(
/// axis: Axis.vertical,
/// sizeFactor: animation,
/// child: GestureDetector(
/// behavior: HitTestBehavior.opaque,
/// onTap: onTap,
/// child: SizedBox(
/// height: 128.0,
/// child: Card(
/// color: Colors.primaries[item % Colors.primaries.length],
/// child: Center(
/// child: Text('Item $item', style: textStyle),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
/// void main() {
/// runApp(AnimatedListSample());
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
/// or removed from a list.
class AnimatedList extends StatefulWidget {
/// Creates a scrolling container that animates items when they are inserted
/// or removed.
const AnimatedList({
Key key,
@required this.itemBuilder,
this.initialItemCount = 0,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0),
super(key: key);
/// Called, as needed, to build list item widgets.
///
/// List items are only built when they're scrolled into view.
///
/// The [AnimatedListItemBuilder] index parameter indicates the item's
/// position in the list. The value of the index parameter will be between 0
/// and [initialItemCount] plus the total number of items that have been
/// inserted with [AnimatedListState.insertItem] and less the total number of
/// items that have been removed with [AnimatedListState.removeItem].
///
/// Implementations of this callback should assume that
/// [AnimatedListState.removeItem] removes an item immediately.
final AnimatedListItemBuilder itemBuilder;
/// {@template flutter.widgets.animatedList.initialItemCount}
/// The number of items the list will start with.
///
/// The appearance of the initial items is not animated. They
/// are created, as needed, by [itemBuilder] with an animation parameter
/// of [kAlwaysCompleteAnimation].
/// {@endtemplate}
final int initialItemCount;
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
final Axis scrollDirection;
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
final ScrollController controller;
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// On iOS, this identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool primary;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions.
final ScrollPhysics physics;
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding;
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [AnimatedList] item widgets that insert
/// or remove items in response to user input.
///
/// ```dart
/// AnimatedListState animatedList = AnimatedList.of(context);
/// ```
static AnimatedListState of(BuildContext context, { bool nullOk = false }) {
assert(context != null);
assert(nullOk != null);
final AnimatedListState result = context.findAncestorStateOfType<AnimatedListState>();
if (nullOk || result != null)
return result;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('AnimatedList.of() called with a context that does not contain an AnimatedList.'),
ErrorDescription('No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().'),
ErrorHint(
'This can happen when the context provided is from the same StatefulWidget that '
'built the AnimatedList. Please see the AnimatedList documentation for examples '
'of how to refer to an AnimatedListState object:'
' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html'
),
context.describeElement('The context used was')
]);
}
@override
AnimatedListState createState() => AnimatedListState();
}
/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [AnimatedList]'s state with a global key:
///
/// ```dart
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
/// ...
/// AnimatedList(key: listKey, ...);
/// ...
/// listKey.currentState.insert(123);
/// ```
///
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method.
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
final GlobalKey<SliverAnimatedListState> _sliverAnimatedListKey = GlobalKey();
/// Insert an item at [index] and start an animation that will be passed
/// to [AnimatedList.itemBuilder] when the item is visible.
///
/// This method's semantics are the same as Dart's [List.insert] method:
/// it increases the length of the list by one and shifts all items at or
/// after [index] towards the end of the list.
void insertItem(int index, { Duration duration = _kDuration }) {
_sliverAnimatedListKey.currentState.insertItem(index, duration: duration);
}
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
/// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [AnimatedList.itemBuilder]. However the
/// item will still appear in the list for [duration] and during that time
/// [builder] must construct its widget as needed.
///
/// This method's semantics are the same as Dart's [List.remove] method:
/// it decreases the length of the list by one and shifts all items at or
/// before [index] towards the beginning of the list.
void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
_sliverAnimatedListKey.currentState.removeItem(index, builder, duration: duration);
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: widget.controller,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
slivers: <Widget>[
SliverPadding(
padding: widget.padding ?? const EdgeInsets.all(0),
sliver: SliverAnimatedList(
key: _sliverAnimatedListKey,
itemBuilder: widget.itemBuilder,
initialItemCount: widget.initialItemCount,
),
),
],
);
}
}
/// A sliver that animates items when they are inserted or removed.
///
/// This widget's [SliverAnimatedListState] can be used to dynamically insert or
/// remove items. To refer to the [SliverAnimatedListState] either provide a
/// [GlobalKey] or use the static [SliverAnimatedList.of] method from an item's
/// input callback.
///
/// {@tool sample --template=freeform}
/// This sample application uses a [SliverAnimatedList] to create an animated
/// effect when items are removed or added to the list.
///
/// ```dart imports
/// import 'package:flutter/foundation.dart';
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
/// void main() => runApp(SliverAnimatedListSample());
///
/// class SliverAnimatedListSample extends StatefulWidget {
/// @override
/// _SliverAnimatedListSampleState createState() => _SliverAnimatedListSampleState();
/// }
///
/// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> {
/// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
/// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
/// ListModel<int> _list;
/// int _selectedItem;
/// int _nextItem; // The next item inserted when the user presses the '+' button.
///
/// @override
/// void initState() {
/// super.initState();
/// _list = ListModel<int>(
/// listKey: _listKey,
/// initialItems: <int>[0, 1, 2],
/// removedItemBuilder: _buildRemovedItem,
/// );
/// _nextItem = 3;
/// }
///
/// // Used to build list items that haven't been removed.
/// Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
/// return CardItem(
/// animation: animation,
/// item: _list[index],
/// selected: _selectedItem == _list[index],
/// onTap: () {
/// setState(() {
/// _selectedItem = _selectedItem == _list[index] ? null : _list[index];
/// });
/// },
/// );
/// }
///
/// // Used to build an item after it has been removed from the list. This
/// // method is needed because a removed item remains visible until its
/// // animation has completed (even though it's gone as far this ListModel is
/// // concerned). The widget will be used by the
/// // [AnimatedListState.removeItem] method's
/// // [AnimatedListRemovedItemBuilder] parameter.
/// Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
/// return CardItem(
/// animation: animation,
/// item: item,
/// selected: false,
/// );
/// }
///
/// // Insert the "next item" into the list model.
/// void _insert() {
/// final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
/// _list.insert(index, _nextItem++);
/// }
///
/// // Remove the selected item from the list model.
/// void _remove() {
/// if (_selectedItem != null) {
/// _list.removeAt(_list.indexOf(_selectedItem));
/// setState(() {
/// _selectedItem = null;
/// });
/// } else {
/// _scaffoldKey.currentState.showSnackBar(SnackBar(
/// content: Text(
/// 'Select an item to remove from the list.',
/// style: TextStyle(fontSize: 20),
/// ),
/// ));
/// }
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Scaffold(
/// key: _scaffoldKey,
/// body: CustomScrollView(
/// slivers: <Widget>[
/// SliverAppBar(
/// title: Text(
/// 'SliverAnimatedList',
/// style: TextStyle(fontSize: 30),
/// ),
/// expandedHeight: 100,
/// centerTitle: true,
/// backgroundColor: Colors.amber[900],
/// leading: IconButton(
/// icon: const Icon(Icons.add_circle),
/// onPressed: _insert,
/// tooltip: 'Insert a new item.',
/// iconSize: 48,
/// ),
/// actions: [
/// IconButton(
/// icon: const Icon(Icons.remove_circle),
/// onPressed: _remove,
/// tooltip: 'Remove the selected item.',
/// iconSize: 48,
/// ),
/// ],
/// ),
/// SliverAnimatedList(
/// key: _listKey,
/// initialItemCount: _list.length,
/// itemBuilder: _buildItem,
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
///
/// // Keeps a Dart [List] in sync with an [AnimatedList].
/// //
/// // The [insert] and [removeAt] methods apply to both the internal list and
/// // the animated list that belongs to [listKey].
/// //
/// // This class only exposes as much of the Dart List API as is needed by the
/// // sample app. More list methods are easily added, however methods that
/// // mutate the list must make the same changes to the animated list in terms
/// // of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
/// class ListModel<E> {
/// ListModel({
/// @required this.listKey,
/// @required this.removedItemBuilder,
/// Iterable<E> initialItems,
/// }) : assert(listKey != null),
/// assert(removedItemBuilder != null),
/// _items = List<E>.from(initialItems ?? <E>[]);
///
/// final GlobalKey<SliverAnimatedListState> listKey;
/// final dynamic removedItemBuilder;
/// final List<E> _items;
///
/// SliverAnimatedListState get _animatedList => listKey.currentState;
///
/// void insert(int index, E item) {
/// _items.insert(index, item);
/// _animatedList.insertItem(index);
/// }
///
/// E removeAt(int index) {
/// final E removedItem = _items.removeAt(index);
/// if (removedItem != null) {
/// _animatedList.removeItem(
/// index,
/// (BuildContext context, Animation<double> animation) => removedItemBuilder(removedItem, context, animation),
/// );
/// }
/// return removedItem;
/// }
///
/// int get length => _items.length;
///
/// E operator [](int index) => _items[index];
///
/// int indexOf(E item) => _items.indexOf(item);
/// }
///
/// // Displays its integer item as 'Item N' on a Card whose color is based on
/// // the item's value.
/// //
/// // The card turns gray when [selected] is true. This widget's height
/// // is based on the [animation] parameter. It varies as the animation value
/// // transitions from 0.0 to 1.0.
/// class CardItem extends StatelessWidget {
/// const CardItem({
/// Key key,
/// @required this.animation,
/// @required this.item,
/// this.onTap,
/// this.selected = false,
/// }) : assert(animation != null),
/// assert(item != null && item >= 0),
/// assert(selected != null),
/// super(key: key);
///
/// final Animation<double> animation;
/// final VoidCallback onTap;
/// final int item;
/// final bool selected;
///
/// @override
/// Widget build(BuildContext context) {
/// return Padding(
/// padding:
/// const EdgeInsets.only(
/// left: 2.0,
/// right: 2.0,
/// top: 2.0,
/// bottom: 0.0,
/// ),
/// child: SizeTransition(
/// axis: Axis.vertical,
/// sizeFactor: animation,
/// child: GestureDetector(
/// onTap: onTap,
/// child: SizedBox(
/// height: 120.0,
/// child: Card(
/// color: selected
/// ? Colors.black12
/// : Colors.primaries[item % Colors.primaries.length],
/// child: Center(
/// child: Text(
/// 'Item $item',
/// style: Theme.of(context).textTheme.headline4,
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [SliverList], which does not animate items when they are inserted or
/// removed.
/// * [AnimatedList], a non-sliver scrolling container that animates items when
/// they are inserted or removed.
class SliverAnimatedList extends StatefulWidget {
/// Creates a sliver that animates items when they are inserted or removed.
const SliverAnimatedList({
Key key,
@required this.itemBuilder,
this.initialItemCount = 0,
}) : assert(itemBuilder != null),
assert(initialItemCount != null && initialItemCount >= 0),
super(key: key);
/// Called, as needed, to build list item widgets.
///
/// List items are only built when they're scrolled into view.
///
/// The [AnimatedListItemBuilder] index parameter indicates the item's
/// position in the list. The value of the index parameter will be between 0
/// and [initialItemCount] plus the total number of items that have been
/// inserted with [SliverAnimatedListState.insertItem] and less the total
/// number of items that have been removed with
/// [SliverAnimatedListState.removeItem].
///
/// Implementations of this callback should assume that
/// [SliverAnimatedListState.removeItem] removes an item immediately.
final AnimatedListItemBuilder itemBuilder;
/// {@macro flutter.widgets.animatedList.initialItemCount}
final int initialItemCount;
@override
SliverAnimatedListState createState() => SliverAnimatedListState();
/// The state from the closest instance of this class that encloses the given
/// context.
///
/// This method is typically used by [SliverAnimatedList] item widgets that
/// insert or remove items in response to user input.
///
/// ```dart
/// SliverAnimatedListState animatedList = SliverAnimatedList.of(context);
/// ```
static SliverAnimatedListState of(BuildContext context, {bool nullOk = false}) {
assert(context != null);
assert(nullOk != null);
final SliverAnimatedListState result = context.findAncestorStateOfType<SliverAnimatedListState>();
if (nullOk || result != null)
return result;
throw FlutterError(
'SliverAnimatedList.of() called with a context that does not contain a SliverAnimatedList.\n'
'No SliverAnimatedListState ancestor could be found starting from the '
'context that was passed to SliverAnimatedListState.of(). This can '
'happen when the context provided is from the same StatefulWidget that '
'built the AnimatedList. Please see the SliverAnimatedList documentation '
'for examples of how to refer to an AnimatedListState object: '
'https://docs.flutter.io/flutter/widgets/SliverAnimatedListState-class.html \n'
'The context used was:\n'
' $context');
}
}
/// The state for a sliver that animates items when they are
/// inserted or removed.
///
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [SliverAnimatedList.itemBuilder] whenever the item's
/// widget is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [SliverAnimatedList]'s state with a global key:
///
/// ```dart
/// GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
/// ...
/// SliverAnimatedList(key: listKey, ...);
/// ...
/// listKey.currentState.insert(123);
/// ```
///
/// [SliverAnimatedList] item input handlers can also refer to their
/// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method.
class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProviderStateMixin {
final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
int _itemsCount = 0;
@override
void initState() {
super.initState();
_itemsCount = widget.initialItemCount;
}
@override
void dispose() {
for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
item.controller.dispose();
}
super.dispose();
}
_ActiveItem _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
final int i = binarySearch(items, _ActiveItem.index(itemIndex));
return i == -1 ? null : items.removeAt(i);
}
_ActiveItem _activeItemAt(List<_ActiveItem> items, int itemIndex) {
final int i = binarySearch(items, _ActiveItem.index(itemIndex));
return i == -1 ? null : items[i];
}
// The insertItem() and removeItem() index parameters are defined as if the
// removeItem() operation removed the corresponding list entry immediately.
// The entry is only actually removed from the ListView when the remove animation
// finishes. The entry is added to _outgoingItems when removeItem is called
// and removed from _outgoingItems when the remove animation finishes.
int _indexToItemIndex(int index) {
int itemIndex = index;
for (final _ActiveItem item in _outgoingItems) {
if (item.itemIndex <= itemIndex)
itemIndex += 1;
else
break;
}
return itemIndex;
}
int _itemIndexToIndex(int itemIndex) {
int index = itemIndex;
for (final _ActiveItem item in _outgoingItems) {
assert(item.itemIndex != itemIndex);
if (item.itemIndex < itemIndex)
index -= 1;
else
break;
}
return index;
}
SliverChildDelegate _createDelegate() {
return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount);
}
/// Insert an item at [index] and start an animation that will be passed to
/// [SliverAnimatedList.itemBuilder] when the item is visible.
///
/// This method's semantics are the same as Dart's [List.insert] method:
/// it increases the length of the list by one and shifts all items at or
/// after [index] towards the end of the list.
void insertItem(int index, { Duration duration = _kDuration }) {
assert(index != null && index >= 0);
assert(duration != null);
final int itemIndex = _indexToItemIndex(index);
assert(itemIndex >= 0 && itemIndex <= _itemsCount);
// Increment the incoming and outgoing item indices to account
// for the insertion.
for (final _ActiveItem item in _incomingItems) {
if (item.itemIndex >= itemIndex)
item.itemIndex += 1;
}
for (final _ActiveItem item in _outgoingItems) {
if (item.itemIndex >= itemIndex)
item.itemIndex += 1;
}
final AnimationController controller = AnimationController(
duration: duration,
vsync: this,
);
final _ActiveItem incomingItem = _ActiveItem.incoming(
controller,
itemIndex,
);
setState(() {
_incomingItems
..add(incomingItem)
..sort();
_itemsCount += 1;
});
controller.forward().then<void>((_) {
_removeActiveItemAt(_incomingItems, incomingItem.itemIndex).controller.dispose();
});
}
/// Remove the item at [index] and start an animation that will be passed
/// to [builder] when the item is visible.
///
/// Items are removed immediately. After an item has been removed, its index
/// will no longer be passed to the [SliverAnimatedList.itemBuilder]. However
/// the item will still appear in the list for [duration] and during that time
/// [builder] must construct its widget as needed.
///
/// This method's semantics are the same as Dart's [List.remove] method:
/// it decreases the length of the list by one and shifts all items at or
/// before [index] towards the beginning of the list.
void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
assert(index != null && index >= 0);
assert(builder != null);
assert(duration != null);
final int itemIndex = _indexToItemIndex(index);
assert(itemIndex >= 0 && itemIndex < _itemsCount);
assert(_activeItemAt(_outgoingItems, itemIndex) == null);
final _ActiveItem incomingItem = _removeActiveItemAt(_incomingItems, itemIndex);
final AnimationController controller = incomingItem?.controller
?? AnimationController(duration: duration, value: 1.0, vsync: this);
final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
setState(() {
_outgoingItems
..add(outgoingItem)
..sort();
});
controller.reverse().then<void>((void value) {
_removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex).controller.dispose();
// Decrement the incoming and outgoing item indices to account
// for the removal.
for (final _ActiveItem item in _incomingItems) {
if (item.itemIndex > outgoingItem.itemIndex)
item.itemIndex -= 1;
}
for (final _ActiveItem item in _outgoingItems) {
if (item.itemIndex > outgoingItem.itemIndex)
item.itemIndex -= 1;
}
setState(() => _itemsCount -= 1);
});
}
Widget _itemBuilder(BuildContext context, int itemIndex) {
final _ActiveItem outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
if (outgoingItem != null) {
return outgoingItem.removedItemBuilder(
context,
outgoingItem.controller.view,
);
}
final _ActiveItem incomingItem = _activeItemAt(_incomingItems, itemIndex);
final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
return widget.itemBuilder(
context,
_itemIndexToIndex(itemIndex),
animation,
);
}
@override
Widget build(BuildContext context) {
return SliverList(
delegate: _createDelegate(),
);
}
}