1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'page_storage.dart';
6/// @docImport 'primary_scroll_controller.dart';
7library;
8
9import 'package:flutter/foundation.dart';
10
11import 'basic.dart';
12import 'framework.dart';
13import 'media_query.dart';
14import 'scroll_controller.dart';
15import 'scroll_delegate.dart';
16import 'scroll_physics.dart';
17import 'scroll_view.dart';
18import 'sliver.dart';
19import 'ticker_provider.dart';
20
21/// A scrolling container that animates items when they are inserted or removed.
22///
23/// This widget's [AnimatedListState] can be used to dynamically insert or
24/// remove items. To refer to the [AnimatedListState] either provide a
25/// [GlobalKey] or use the static [of] method from an item's input callback.
26///
27/// This widget is similar to one created by [ListView.builder].
28///
29/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
30///
31/// {@tool dartpad}
32/// This sample application uses an [AnimatedList] to create an effect when
33/// items are removed or added to the list.
34///
35/// ** See code in examples/api/lib/widgets/animated_list/animated_list.0.dart **
36/// {@end-tool}
37///
38/// By default, [AnimatedList] will automatically pad the limits of the
39/// list's scrollable to avoid partial obstructions indicated by
40/// [MediaQuery]'s padding. To avoid this behavior, override with a
41/// zero [padding] property.
42///
43/// {@tool snippet}
44/// The following example demonstrates how to override the default top and
45/// bottom padding using [MediaQuery.removePadding].
46///
47/// ```dart
48/// Widget myWidget(BuildContext context) {
49/// return MediaQuery.removePadding(
50/// context: context,
51/// removeTop: true,
52/// removeBottom: true,
53/// child: AnimatedList(
54/// initialItemCount: 50,
55/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
56/// return Card(
57/// color: Colors.amber,
58/// child: Center(child: Text('$index')),
59/// );
60/// }
61/// ),
62/// );
63/// }
64/// ```
65/// {@end-tool}
66///
67/// See also:
68///
69/// * [SliverAnimatedList], a sliver that animates items when they are inserted
70/// or removed from a list.
71/// * [SliverAnimatedGrid], a sliver which animates items when they are
72/// inserted or removed from a grid.
73/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
74/// they are inserted or removed in a grid.
75class AnimatedList extends _AnimatedScrollView {
76 /// Creates a scrolling container that animates items when they are inserted
77 /// or removed.
78 const AnimatedList({
79 super.key,
80 required super.itemBuilder,
81 super.initialItemCount = 0,
82 super.scrollDirection = Axis.vertical,
83 super.reverse = false,
84 super.controller,
85 super.primary,
86 super.physics,
87 super.shrinkWrap = false,
88 super.padding,
89 super.clipBehavior = Clip.hardEdge,
90 }) : assert(initialItemCount >= 0);
91
92 /// A scrolling container that animates items with separators when they are inserted or removed.
93 ///
94 /// This widget's [AnimatedListState] can be used to dynamically insert or
95 /// remove items. To refer to the [AnimatedListState] either provide a
96 /// [GlobalKey] or use the static [of] method from an item's input callback.
97 ///
98 /// This widget is similar to one created by [ListView.separated].
99 ///
100 /// {@tool dartpad}
101 /// This sample application uses an [AnimatedList.separated] to create an effect when
102 /// items are removed or added to the list.
103 ///
104 /// ** See code in examples/api/lib/widgets/animated_list/animated_list_separated.0.dart **
105 /// {@end-tool}
106 ///
107 /// By default, [AnimatedList.separated] will automatically pad the limits of the
108 /// list's scrollable to avoid partial obstructions indicated by
109 /// [MediaQuery]'s padding. To avoid this behavior, override with a
110 /// zero [padding] property.
111 ///
112 /// {@tool snippet}
113 /// The following example demonstrates how to override the default top and
114 /// bottom padding using [MediaQuery.removePadding].
115 ///
116 /// ```dart
117 /// Widget myWidget(BuildContext context) {
118 /// return MediaQuery.removePadding(
119 /// context: context,
120 /// removeTop: true,
121 /// removeBottom: true,
122 /// child: AnimatedList.separated(
123 /// initialItemCount: 50,
124 /// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
125 /// return Card(
126 /// color: Colors.amber,
127 /// child: Center(child: Text('$index')),
128 /// );
129 /// },
130 /// separatorBuilder: (BuildContext context, int index, Animation<double> animation) {
131 /// return const Divider();
132 /// },
133 /// removedSeparatorBuilder: (BuildContext context, int index, Animation<double> animation) {
134 /// return const Divider();
135 /// }
136 /// ),
137 /// );
138 /// }
139 /// ```
140 /// {@end-tool}
141 ///
142 /// See also:
143 ///
144 /// * [SliverAnimatedList], a sliver that animates items when they are inserted
145 /// or removed from a list.
146 /// * [SliverAnimatedGrid], a sliver which animates items when they are
147 /// inserted or removed from a grid.
148 /// * [AnimatedGrid], a non-sliver scrolling container that animates items when
149 /// they are inserted or removed in a grid.
150 /// * [AnimatedList], which animates items added and removed from a list instead
151 /// of a grid.
152 AnimatedList.separated({
153 super.key,
154 required AnimatedItemBuilder itemBuilder,
155 required AnimatedItemBuilder separatorBuilder,
156 required AnimatedItemBuilder super.removedSeparatorBuilder,
157 int initialItemCount = 0,
158 super.scrollDirection = Axis.vertical,
159 super.reverse = false,
160 super.controller,
161 super.primary,
162 super.physics,
163 super.shrinkWrap = false,
164 super.padding,
165 super.clipBehavior = Clip.hardEdge,
166 }) : assert(initialItemCount >= 0),
167 super(
168 initialItemCount: _computeChildCountWithSeparators(initialItemCount),
169 itemBuilder: (BuildContext context, int index, Animation<double> animation) {
170 final int itemIndex = index ~/ 2;
171 if (index.isEven) {
172 return itemBuilder(context, itemIndex, animation);
173 }
174 return separatorBuilder(context, itemIndex, animation);
175 },
176 );
177
178 /// The state from the closest instance of this class that encloses the given
179 /// context.
180 ///
181 /// This method is typically used by [AnimatedList] item widgets that insert
182 /// or remove items in response to user input.
183 ///
184 /// If no [AnimatedList] surrounds the context given, then this function will
185 /// assert in debug mode and throw an exception in release mode.
186 ///
187 /// This method can be expensive (it walks the element tree).
188 ///
189 /// This method does not create a dependency, and so will not cause rebuilding
190 /// when the state changes.
191 ///
192 /// See also:
193 ///
194 /// * [maybeOf], a similar function that will return null if no
195 /// [AnimatedList] ancestor is found.
196 static AnimatedListState of(BuildContext context) {
197 final AnimatedListState? result = AnimatedList.maybeOf(context);
198 assert(() {
199 if (result == null) {
200 throw FlutterError.fromParts(<DiagnosticsNode>[
201 ErrorSummary(
202 'AnimatedList.of() called with a context that does not contain an AnimatedList.',
203 ),
204 ErrorDescription(
205 'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().',
206 ),
207 ErrorHint(
208 'This can happen when the context provided is from the same StatefulWidget that '
209 'built the AnimatedList. Please see the AnimatedList documentation for examples '
210 'of how to refer to an AnimatedListState object:\n'
211 ' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html',
212 ),
213 context.describeElement('The context used was'),
214 ]);
215 }
216 return true;
217 }());
218 return result!;
219 }
220
221 /// The [AnimatedListState] from the closest instance of [AnimatedList] that encloses the given
222 /// context.
223 ///
224 /// This method is typically used by [AnimatedList] item widgets that insert
225 /// or remove items in response to user input.
226 ///
227 /// If no [AnimatedList] surrounds the context given, then this function will
228 /// return null.
229 ///
230 /// This method can be expensive (it walks the element tree).
231 ///
232 /// This method does not create a dependency, and so will not cause rebuilding
233 /// when the state changes.
234 ///
235 /// See also:
236 ///
237 /// * [of], a similar function that will throw if no [AnimatedList] ancestor
238 /// is found.
239 static AnimatedListState? maybeOf(BuildContext context) {
240 return context.findAncestorStateOfType<AnimatedListState>();
241 }
242
243 // Helper method to compute the actual child count when taking separators into account.
244 static int _computeChildCountWithSeparators(int itemCount) {
245 if (itemCount == 0) {
246 return 0;
247 }
248 return itemCount * 2 - 1;
249 }
250
251 @override
252 AnimatedListState createState() => AnimatedListState();
253}
254
255/// The [AnimatedListState] for [AnimatedList], a scrolling list container that
256/// animates items when they are inserted or removed.
257///
258/// When an item is inserted with [insertItem] an animation begins running. The
259/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
260/// is needed.
261///
262/// When multiple items are inserted with [insertAllItems] an animation begins running.
263/// The animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
264/// is needed.
265///
266/// If using [AnimatedList.separated], the animation is also passed to
267/// `AnimatedList.separatorBuilder` whenever the separator's widget is needed.
268///
269/// When an item is removed with [removeItem] its animation is reversed.
270/// The removed item's animation is passed to the [removeItem] builder
271/// parameter. If using [AnimatedList.separated], the corresponding separator's
272/// animation is also passed to the [AnimatedList.removedSeparatorBuilder] parameter.
273///
274/// An app that needs to insert or remove items in response to an event
275/// can refer to the [AnimatedList]'s state with a global key:
276///
277/// ```dart
278/// // (e.g. in a stateful widget)
279/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
280///
281/// // ...
282///
283/// @override
284/// Widget build(BuildContext context) {
285/// return AnimatedList(
286/// key: listKey,
287/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
288/// return const Placeholder();
289/// },
290/// );
291/// }
292///
293/// // ...
294///
295/// void _updateList() {
296/// // adds "123" to the AnimatedList
297/// listKey.currentState!.insertItem(123);
298/// }
299/// ```
300///
301/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
302/// with the static [AnimatedList.of] method.
303class AnimatedListState extends _AnimatedScrollViewState<AnimatedList> {
304 @protected
305 @override
306 Widget build(BuildContext context) {
307 return _wrap(
308 SliverAnimatedList(
309 key: _sliverAnimatedMultiBoxKey,
310 itemBuilder: widget.itemBuilder,
311 initialItemCount: widget.initialItemCount,
312 ),
313 widget.scrollDirection,
314 );
315 }
316}
317
318/// A scrolling container that animates items when they are inserted into or removed from a grid.
319/// in a grid.
320///
321/// This widget's [AnimatedGridState] can be used to dynamically insert or
322/// remove items. To refer to the [AnimatedGridState] either provide a
323/// [GlobalKey] or use the static [of] method from an item's input callback.
324///
325/// This widget is similar to one created by [GridView.builder].
326///
327/// {@tool dartpad}
328/// This sample application uses an [AnimatedGrid] to create an effect when
329/// items are removed or added to the grid.
330///
331/// ** See code in examples/api/lib/widgets/animated_grid/animated_grid.0.dart **
332/// {@end-tool}
333///
334/// By default, [AnimatedGrid] will automatically pad the limits of the
335/// grid's scrollable to avoid partial obstructions indicated by
336/// [MediaQuery]'s padding. To avoid this behavior, override with a
337/// zero [padding] property.
338///
339/// {@tool snippet}
340/// The following example demonstrates how to override the default top and
341/// bottom padding using [MediaQuery.removePadding].
342///
343/// ```dart
344/// Widget myWidget(BuildContext context) {
345/// return MediaQuery.removePadding(
346/// context: context,
347/// removeTop: true,
348/// removeBottom: true,
349/// child: AnimatedGrid(
350/// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
351/// crossAxisCount: 3,
352/// ),
353/// initialItemCount: 50,
354/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
355/// return Card(
356/// color: Colors.amber,
357/// child: Center(child: Text('$index')),
358/// );
359/// }
360/// ),
361/// );
362/// }
363/// ```
364/// {@end-tool}
365///
366/// See also:
367///
368/// * [SliverAnimatedGrid], a sliver which animates items when they are inserted
369/// into or removed from a grid.
370/// * [SliverAnimatedList], a sliver which animates items added and removed from
371/// a list instead of a grid.
372/// * [AnimatedList], which animates items added and removed from a list instead
373/// of a grid.
374class AnimatedGrid extends _AnimatedScrollView {
375 /// Creates a scrolling container that animates items when they are inserted
376 /// or removed.
377 const AnimatedGrid({
378 super.key,
379 required super.itemBuilder,
380 required this.gridDelegate,
381 super.initialItemCount = 0,
382 super.scrollDirection = Axis.vertical,
383 super.reverse = false,
384 super.controller,
385 super.primary,
386 super.physics,
387 super.padding,
388 super.clipBehavior = Clip.hardEdge,
389 }) : assert(initialItemCount >= 0);
390
391 /// {@template flutter.widgets.AnimatedGrid.gridDelegate}
392 /// A delegate that controls the layout of the children within the
393 /// [AnimatedGrid].
394 ///
395 /// See also:
396 ///
397 /// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with
398 /// a fixed number of tiles in the cross axis.
399 /// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
400 /// tiles that have a maximum cross-axis extent.
401 /// {@endtemplate}
402 final SliverGridDelegate gridDelegate;
403
404 /// The state from the closest instance of this class that encloses the given
405 /// context.
406 ///
407 /// This method is typically used by [AnimatedGrid] item widgets that insert
408 /// or remove items in response to user input.
409 ///
410 /// If no [AnimatedGrid] surrounds the context given, then this function will
411 /// assert in debug mode and throw an exception in release mode.
412 ///
413 /// This method can be expensive (it walks the element tree).
414 ///
415 /// This method does not create a dependency, and so will not cause rebuilding
416 /// when the state changes.
417 ///
418 /// See also:
419 ///
420 /// * [maybeOf], a similar function that will return null if no
421 /// [AnimatedGrid] ancestor is found.
422 static AnimatedGridState of(BuildContext context) {
423 final AnimatedGridState? result = AnimatedGrid.maybeOf(context);
424 assert(() {
425 if (result == null) {
426 throw FlutterError.fromParts(<DiagnosticsNode>[
427 ErrorSummary(
428 'AnimatedGrid.of() called with a context that does not contain an AnimatedGrid.',
429 ),
430 ErrorDescription(
431 'No AnimatedGrid ancestor could be found starting from the context that was passed to AnimatedGrid.of().',
432 ),
433 ErrorHint(
434 'This can happen when the context provided is from the same StatefulWidget that '
435 'built the AnimatedGrid. Please see the AnimatedGrid documentation for examples '
436 'of how to refer to an AnimatedGridState object:\n'
437 ' https://api.flutter.dev/flutter/widgets/AnimatedGridState-class.html',
438 ),
439 context.describeElement('The context used was'),
440 ]);
441 }
442 return true;
443 }());
444 return result!;
445 }
446
447 /// The state from the closest instance of this class that encloses the given
448 /// context.
449 ///
450 /// This method is typically used by [AnimatedGrid] item widgets that insert
451 /// or remove items in response to user input.
452 ///
453 /// If no [AnimatedGrid] surrounds the context given, then this function will
454 /// return null.
455 ///
456 /// This method can be expensive (it walks the element tree).
457 ///
458 /// This method does not create a dependency, and so will not cause rebuilding
459 /// when the state changes.
460 ///
461 /// See also:
462 ///
463 /// * [of], a similar function that will throw if no [AnimatedGrid] ancestor
464 /// is found.
465 static AnimatedGridState? maybeOf(BuildContext context) {
466 return context.findAncestorStateOfType<AnimatedGridState>();
467 }
468
469 @override
470 AnimatedGridState createState() => AnimatedGridState();
471}
472
473/// The [State] for an [AnimatedGrid] that animates items when they are
474/// inserted or removed.
475///
476/// When an item is inserted with [insertItem] an animation begins running. The
477/// animation is passed to [AnimatedGrid.itemBuilder] whenever the item's widget
478/// is needed.
479///
480/// When an item is removed with [removeItem] its animation is reversed.
481/// The removed item's animation is passed to the [removeItem] builder
482/// parameter.
483///
484/// An app that needs to insert or remove items in response to an event
485/// can refer to the [AnimatedGrid]'s state with a global key:
486///
487/// ```dart
488/// // (e.g. in a stateful widget)
489/// GlobalKey<AnimatedGridState> gridKey = GlobalKey<AnimatedGridState>();
490///
491/// // ...
492///
493/// @override
494/// Widget build(BuildContext context) {
495/// return AnimatedGrid(
496/// key: gridKey,
497/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
498/// return const Placeholder();
499/// },
500/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100.0),
501/// );
502/// }
503///
504/// // ...
505///
506/// void _updateGrid() {
507/// // adds "123" to the AnimatedGrid
508/// gridKey.currentState!.insertItem(123);
509/// }
510/// ```
511///
512/// [AnimatedGrid] item input handlers can also refer to their [AnimatedGridState]
513/// with the static [AnimatedGrid.of] method.
514class AnimatedGridState extends _AnimatedScrollViewState<AnimatedGrid> {
515 @protected
516 @override
517 Widget build(BuildContext context) {
518 return _wrap(
519 SliverAnimatedGrid(
520 key: _sliverAnimatedMultiBoxKey,
521 gridDelegate: widget.gridDelegate,
522 itemBuilder: widget.itemBuilder,
523 initialItemCount: widget.initialItemCount,
524 ),
525 widget.scrollDirection,
526 );
527 }
528}
529
530abstract class _AnimatedScrollView extends StatefulWidget {
531 /// Creates a scrolling container that animates items when they are inserted
532 /// or removed.
533 const _AnimatedScrollView({
534 super.key,
535 required this.itemBuilder,
536 this.removedSeparatorBuilder,
537 this.initialItemCount = 0,
538 this.scrollDirection = Axis.vertical,
539 this.reverse = false,
540 this.controller,
541 this.primary,
542 this.physics,
543 this.shrinkWrap = false,
544 this.padding,
545 this.clipBehavior = Clip.hardEdge,
546 }) : assert(initialItemCount >= 0);
547
548 /// {@template flutter.widgets.AnimatedScrollView.itemBuilder}
549 /// Called, as needed, to build children widgets.
550 ///
551 /// Children are only built when they're scrolled into view.
552 ///
553 /// The [AnimatedItemBuilder] index parameter indicates the item's
554 /// position in the scroll view. The value of the index parameter will be
555 /// between 0 and [initialItemCount] plus the total number of items that have
556 /// been inserted with [AnimatedListState.insertItem] or
557 /// [AnimatedGridState.insertItem] and less the total number of items that
558 /// have been removed with [AnimatedListState.removeItem] or
559 /// [AnimatedGridState.removeItem].
560 ///
561 /// Implementations of this callback should assume that
562 /// `removeItem` removes an item immediately.
563 /// {@endtemplate}
564 final AnimatedItemBuilder itemBuilder;
565
566 /// {@template flutter.widgets.AnimatedScrollView.removedSeparatorBuilder}
567 /// Called, as needed, to build separator widgets.
568 ///
569 /// Separators are only built when they're scrolled into view.
570 ///
571 /// The [AnimatedItemBuilder] index parameter indicates the
572 /// separator's corresponding item's position in the scroll view. The value
573 /// of the index parameter will be between 0 and [initialItemCount] plus the
574 /// total number of items that have been inserted with [AnimatedListState.insertItem]
575 /// and less the total number of items that have been removed with [AnimatedListState.removeItem].
576 ///
577 /// Implementations of this callback should assume that
578 /// `removeItem` removes an item immediately.
579 /// {@endtemplate}
580 final AnimatedItemBuilder? removedSeparatorBuilder;
581
582 /// {@template flutter.widgets.AnimatedScrollView.initialItemCount}
583 /// The number of items the [AnimatedList] or [AnimatedGrid] will start with.
584 ///
585 /// The appearance of the initial items is not animated. They
586 /// are created, as needed, by [itemBuilder] with an animation parameter
587 /// of [kAlwaysCompleteAnimation].
588 /// {@endtemplate}
589 final int initialItemCount;
590
591 /// {@macro flutter.widgets.scroll_view.scrollDirection}
592 final Axis scrollDirection;
593
594 /// Whether the scroll view scrolls in the reading direction.
595 ///
596 /// For example, if the reading direction is left-to-right and
597 /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
598 /// left to right when [reverse] is false and from right to left when
599 /// [reverse] is true.
600 ///
601 /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
602 /// scrolls from top to bottom when [reverse] is false and from bottom to top
603 /// when [reverse] is true.
604 ///
605 /// Defaults to false.
606 final bool reverse;
607
608 /// An object that can be used to control the position to which this scroll
609 /// view is scrolled.
610 ///
611 /// Must be null if [primary] is true.
612 ///
613 /// A [ScrollController] serves several purposes. It can be used to control
614 /// the initial scroll position (see [ScrollController.initialScrollOffset]).
615 /// It can be used to control whether the scroll view should automatically
616 /// save and restore its scroll position in the [PageStorage] (see
617 /// [ScrollController.keepScrollOffset]). It can be used to read the current
618 /// scroll position (see [ScrollController.offset]), or change it (see
619 /// [ScrollController.animateTo]).
620 final ScrollController? controller;
621
622 /// Whether this is the primary scroll view associated with the parent
623 /// [PrimaryScrollController].
624 ///
625 /// On iOS, this identifies the scroll view that will scroll to top in
626 /// response to a tap in the status bar.
627 ///
628 /// Defaults to true when [scrollDirection] is [Axis.vertical] and
629 /// [controller] is null.
630 final bool? primary;
631
632 /// How the scroll view should respond to user input.
633 ///
634 /// For example, this determines how the scroll view continues to animate after the
635 /// user stops dragging the scroll view.
636 ///
637 /// Defaults to matching platform conventions.
638 final ScrollPhysics? physics;
639
640 /// Whether the extent of the scroll view in the [scrollDirection] should be
641 /// determined by the contents being viewed.
642 ///
643 /// If the scroll view does not shrink wrap, then the scroll view will expand
644 /// to the maximum allowed size in the [scrollDirection]. If the scroll view
645 /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
646 /// be true.
647 ///
648 /// Shrink wrapping the content of the scroll view is significantly more
649 /// expensive than expanding to the maximum allowed size because the content
650 /// can expand and contract during scrolling, which means the size of the
651 /// scroll view needs to be recomputed whenever the scroll position changes.
652 ///
653 /// Defaults to false.
654 final bool shrinkWrap;
655
656 /// The amount of space by which to inset the children.
657 final EdgeInsetsGeometry? padding;
658
659 /// {@macro flutter.material.Material.clipBehavior}
660 ///
661 /// Defaults to [Clip.hardEdge].
662 final Clip clipBehavior;
663}
664
665abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends State<T>
666 with TickerProviderStateMixin {
667 final GlobalKey<_SliverAnimatedMultiBoxAdaptorState<_SliverAnimatedMultiBoxAdaptor>>
668 _sliverAnimatedMultiBoxKey = GlobalKey();
669
670 /// Insert an item at [index] and start an animation that will be passed
671 /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the item
672 /// is visible.
673 ///
674 /// If using [AnimatedList.separated] the animation will also be passed
675 /// to `separatorBuilder`.
676 ///
677 /// This method's semantics are the same as Dart's [List.insert] method: it
678 /// increases the length of the list of items by one and shifts
679 /// all items at or after [index] towards the end of the list of items.
680 void insertItem(int index, {Duration duration = _kDuration}) {
681 if (widget.removedSeparatorBuilder == null) {
682 _sliverAnimatedMultiBoxKey.currentState!.insertItem(index, duration: duration);
683 } else {
684 final int itemIndex = _computeItemIndex(index);
685 _sliverAnimatedMultiBoxKey.currentState!.insertItem(itemIndex, duration: duration);
686 if (_itemsCount > 1) {
687 // Because `insertItem` moves the items after the index, we need to insert the separator
688 // at the same index as the item. If there is only one item, we don't need to insert a separator.
689 _sliverAnimatedMultiBoxKey.currentState!.insertItem(itemIndex, duration: duration);
690 }
691 }
692 }
693
694 /// Insert multiple items at [index] and start an animation that will be passed
695 /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
696 /// are visible.
697 ///
698 /// If using [AnimatedList.separated] the animation will also be passed to `separatorBuilder`.
699 void insertAllItems(
700 int index,
701 int length, {
702 Duration duration = _kDuration,
703 bool isAsync = false,
704 }) {
705 if (widget.removedSeparatorBuilder == null) {
706 _sliverAnimatedMultiBoxKey.currentState!.insertAllItems(index, length, duration: duration);
707 } else {
708 final int itemIndex = _computeItemIndex(index);
709 final int lengthWithSeparators = _itemsCount == 0 ? length * 2 - 1 : length * 2;
710 _sliverAnimatedMultiBoxKey.currentState!.insertAllItems(
711 itemIndex,
712 lengthWithSeparators,
713 duration: duration,
714 );
715 }
716 }
717
718 /// Remove the item at [index] and start an animation that will be passed to
719 /// [builder] when the item is visible.
720 ///
721 /// If using [AnimatedList.separated], the animation will also be passed to the
722 /// corresponding separator's [AnimatedList.removedSeparatorBuilder].
723 ///
724 /// Items are removed immediately. After an item has been removed, its index
725 /// will no longer be passed to the [builder]. However, the
726 /// item will still appear for [duration] and during that time
727 /// [builder] must construct its widget as needed.
728 ///
729 /// This method's semantics are the same as Dart's [List.remove] method: it
730 /// decreases the length of items by one and shifts all items at or before
731 /// [index] towards the beginning of the list of items.
732 ///
733 /// See also:
734 ///
735 /// * [AnimatedRemovedItemBuilder], which describes the arguments to the
736 /// [builder] argument.
737 void removeItem(int index, AnimatedRemovedItemBuilder builder, {Duration duration = _kDuration}) {
738 final AnimatedItemBuilder? removedSeparatorBuilder = widget.removedSeparatorBuilder;
739 if (removedSeparatorBuilder == null) {
740 // There are no separators. Remove only the item.
741 _sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
742 } else {
743 final int itemIndex = _computeItemIndex(index);
744 // Remove the item
745 _sliverAnimatedMultiBoxKey.currentState!.removeItem(itemIndex, builder, duration: duration);
746 if (_itemsCount > 1) {
747 if (itemIndex == _itemsCount - 1) {
748 // The item was removed from the end of the list, so the separator to remove is the one at `last index` - 1.
749 _sliverAnimatedMultiBoxKey.currentState!.removeItem(
750 itemIndex - 1,
751 _toRemovedItemBuilder(removedSeparatorBuilder, index - 1),
752 duration: duration,
753 );
754 } else {
755 // The item was removed from the middle or beginning of the list,
756 // so the corresponding separator took its place and needs to be removed at `itemIndex`.
757 _sliverAnimatedMultiBoxKey.currentState!.removeItem(
758 itemIndex,
759 _toRemovedItemBuilder(removedSeparatorBuilder, index),
760 duration: duration,
761 );
762 }
763 }
764 }
765 }
766
767 /// Remove all the items and start an animation that will be passed to
768 /// [builder] when the items are visible.
769 ///
770 /// If using [AnimatedList.separated], the animation will also be passed
771 /// to the corresponding separator's [AnimatedList.removedSeparatorBuilder].
772 ///
773 /// Items are removed immediately. However, the
774 /// items will still appear for [duration], and during that time
775 /// [builder] must construct its widget as needed.
776 ///
777 /// This method's semantics are the same as Dart's [List.clear] method: it
778 /// removes all the items in the list.
779 ///
780 /// See also:
781 ///
782 /// * [AnimatedRemovedItemBuilder], which describes the arguments to the
783 /// [builder] argument.
784 void removeAllItems(AnimatedRemovedItemBuilder builder, {Duration duration = _kDuration}) {
785 final AnimatedItemBuilder? removedSeparatorBuilder = widget.removedSeparatorBuilder;
786 if (removedSeparatorBuilder == null) {
787 // There are no separators. We can remove all items with the same builder.
788 _sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
789 return;
790 }
791
792 // There are separators. We need to remove items and separators separately
793 // with the corresponding builders.
794 for (int index = _itemsCount - 1; index >= 0; index--) {
795 if (index.isEven) {
796 _sliverAnimatedMultiBoxKey.currentState!.removeItem(index, builder, duration: duration);
797 } else {
798 // The index of the separator's corresponding item
799 final int itemIndex = index ~/ 2;
800 _sliverAnimatedMultiBoxKey.currentState!.removeItem(
801 index,
802 _toRemovedItemBuilder(removedSeparatorBuilder, itemIndex),
803 duration: duration,
804 );
805 }
806 }
807 }
808
809 int get _itemsCount => _sliverAnimatedMultiBoxKey.currentState!._itemsCount;
810
811 // Helper method to compute the index for the item to insert or remove considering the separators in between.
812 int _computeItemIndex(int index) {
813 if (index == 0) {
814 return index;
815 }
816 final int itemsAndSeparatorsCount = _itemsCount;
817 final int separatorsCount = itemsAndSeparatorsCount ~/ 2;
818 final int separatedItemsCount = _itemsCount - separatorsCount;
819
820 final bool isNewLastIndex = index == separatedItemsCount;
821 final int indexAdjustedForSeparators = index * 2;
822 return isNewLastIndex ? indexAdjustedForSeparators - 1 : indexAdjustedForSeparators;
823 }
824
825 // Helper method to create an [AnimatedRemovedItemBuilder]
826 // from an [AnimatedItemBuilder] for given [index].
827 AnimatedRemovedItemBuilder _toRemovedItemBuilder(AnimatedItemBuilder builder, int index) {
828 return (BuildContext context, Animation<double> animation) {
829 return builder(context, index, animation);
830 };
831 }
832
833 Widget _wrap(Widget sliver, Axis direction) {
834 EdgeInsetsGeometry? effectivePadding = widget.padding;
835 if (widget.padding == null) {
836 final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
837 if (mediaQuery != null) {
838 // Automatically pad sliver with padding from MediaQuery.
839 final EdgeInsets mediaQueryHorizontalPadding = mediaQuery.padding.copyWith(
840 top: 0.0,
841 bottom: 0.0,
842 );
843 final EdgeInsets mediaQueryVerticalPadding = mediaQuery.padding.copyWith(
844 left: 0.0,
845 right: 0.0,
846 );
847 // Consume the main axis padding with SliverPadding.
848 effectivePadding = direction == Axis.vertical
849 ? mediaQueryVerticalPadding
850 : mediaQueryHorizontalPadding;
851 // Leave behind the cross axis padding.
852 sliver = MediaQuery(
853 data: mediaQuery.copyWith(
854 padding: direction == Axis.vertical
855 ? mediaQueryHorizontalPadding
856 : mediaQueryVerticalPadding,
857 ),
858 child: sliver,
859 );
860 }
861 }
862
863 if (effectivePadding != null) {
864 sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
865 }
866 return CustomScrollView(
867 scrollDirection: widget.scrollDirection,
868 reverse: widget.reverse,
869 controller: widget.controller,
870 primary: widget.primary,
871 physics: widget.physics,
872 clipBehavior: widget.clipBehavior,
873 shrinkWrap: widget.shrinkWrap,
874 slivers: <Widget>[sliver],
875 );
876 }
877}
878
879/// Signature for the builder callback used by [AnimatedList], [AnimatedList.separated]
880/// & [AnimatedGrid] to build their animated children.
881///
882/// This signature is also used by [AnimatedList.separated] to build its separators and
883/// to animate their exit transition after their corresponding item has been removed.
884///
885/// The [context] argument is the build context where the widget will be
886/// created, the [index] is the index of the item to be built, and the
887/// [animation] is an [Animation] that should be used to animate an entry
888/// transition for the widget that is built.
889///
890/// For [AnimatedList.separated], the [index] is the index
891/// of the corresponding item of the separator that is built or removed.
892/// For [AnimatedList.separated] `removedSeparatorBuilder`, the [animation] should be used
893/// to animate an exit transition for the widget that is built.
894///
895/// See also:
896///
897/// * [AnimatedRemovedItemBuilder], a builder that is for removing items with
898/// animations instead of adding them.
899typedef AnimatedItemBuilder =
900 Widget Function(BuildContext context, int index, Animation<double> animation);
901
902/// Signature for the builder callback used in [AnimatedListState.removeItem] and
903/// [AnimatedGridState.removeItem] to animate their children after they have
904/// been removed.
905///
906/// The [context] argument is the build context where the widget will be
907/// created, and the [animation] is an [Animation] that should be used to
908/// animate an exit transition for the widget that is built.
909///
910/// See also:
911///
912/// * [AnimatedItemBuilder], a builder that is for adding items with animations
913/// instead of removing them.
914typedef AnimatedRemovedItemBuilder =
915 Widget Function(BuildContext context, Animation<double> animation);
916
917// The default insert/remove animation duration.
918const Duration _kDuration = Duration(milliseconds: 300);
919
920// Incoming and outgoing animated items.
921class _ActiveItem implements Comparable<_ActiveItem> {
922 _ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null;
923 _ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder);
924 _ActiveItem.index(this.itemIndex) : controller = null, removedItemBuilder = null;
925
926 final AnimationController? controller;
927 final AnimatedRemovedItemBuilder? removedItemBuilder;
928 int itemIndex;
929
930 @override
931 int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
932}
933
934/// A [SliverList] that animates items when they are inserted or removed.
935///
936/// This widget's [SliverAnimatedListState] can be used to dynamically insert or
937/// remove items. To refer to the [SliverAnimatedListState] either provide a
938/// [GlobalKey] or use the static [SliverAnimatedList.of] method from a list item's
939/// input callback.
940///
941/// {@tool dartpad}
942/// This sample application uses a [SliverAnimatedList] to create an animated
943/// effect when items are removed or added to the list.
944///
945/// ** See code in examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart **
946/// {@end-tool}
947///
948/// See also:
949///
950/// * [SliverList], which does not animate items when they are inserted or
951/// removed.
952/// * [AnimatedList], a non-sliver scrolling container that animates items when
953/// they are inserted or removed.
954/// * [SliverAnimatedGrid], a sliver which animates items when they are
955/// inserted into or removed from a grid.
956/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
957/// they are inserted into or removed from a grid.
958class SliverAnimatedList extends _SliverAnimatedMultiBoxAdaptor {
959 /// Creates a [SliverList] that animates items when they are inserted or
960 /// removed.
961 const SliverAnimatedList({
962 super.key,
963 required super.itemBuilder,
964 super.findChildIndexCallback,
965 super.initialItemCount = 0,
966 }) : assert(initialItemCount >= 0);
967
968 @override
969 SliverAnimatedListState createState() => SliverAnimatedListState();
970
971 /// The [SliverAnimatedListState] from the closest instance of this class that encloses the given
972 /// context.
973 ///
974 /// This method is typically used by [SliverAnimatedList] item widgets that
975 /// insert or remove items in response to user input.
976 ///
977 /// If no [SliverAnimatedList] surrounds the context given, then this function
978 /// will assert in debug mode and throw an exception in release mode.
979 ///
980 /// This method can be expensive (it walks the element tree).
981 ///
982 /// This method does not create a dependency, and so will not cause rebuilding
983 /// when the state changes.
984 ///
985 /// See also:
986 ///
987 /// * [maybeOf], a similar function that will return null if no
988 /// [SliverAnimatedList] ancestor is found.
989 static SliverAnimatedListState of(BuildContext context) {
990 final SliverAnimatedListState? result = SliverAnimatedList.maybeOf(context);
991 assert(() {
992 if (result == null) {
993 throw FlutterError(
994 'SliverAnimatedList.of() called with a context that does not contain a SliverAnimatedList.\n'
995 'No SliverAnimatedListState ancestor could be found starting from the '
996 'context that was passed to SliverAnimatedListState.of(). This can '
997 'happen when the context provided is from the same StatefulWidget that '
998 'built the AnimatedList. Please see the SliverAnimatedList documentation '
999 'for examples of how to refer to an AnimatedListState object: '
1000 'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n'
1001 'The context used was:\n'
1002 ' $context',
1003 );
1004 }
1005 return true;
1006 }());
1007 return result!;
1008 }
1009
1010 /// The [SliverAnimatedListState] from the closest instance of this class that encloses the given
1011 /// context.
1012 ///
1013 /// This method is typically used by [SliverAnimatedList] item widgets that
1014 /// insert or remove items in response to user input.
1015 ///
1016 /// If no [SliverAnimatedList] surrounds the context given, then this function
1017 /// will return null.
1018 ///
1019 /// This method can be expensive (it walks the element tree).
1020 ///
1021 /// This method does not create a dependency, and so will not cause rebuilding
1022 /// when the state changes.
1023 ///
1024 /// See also:
1025 ///
1026 /// * [of], a similar function that will throw if no [SliverAnimatedList]
1027 /// ancestor is found.
1028 static SliverAnimatedListState? maybeOf(BuildContext context) {
1029 return context.findAncestorStateOfType<SliverAnimatedListState>();
1030 }
1031}
1032
1033/// The state for a [SliverAnimatedList] that animates items when they are
1034/// inserted or removed.
1035///
1036/// When an item is inserted with [insertItem] an animation begins running. The
1037/// animation is passed to [SliverAnimatedList.itemBuilder] whenever the item's
1038/// widget is needed.
1039///
1040/// When an item is removed with [removeItem] its animation is reversed.
1041/// The removed item's animation is passed to the [removeItem] builder
1042/// parameter.
1043///
1044/// An app that needs to insert or remove items in response to an event
1045/// can refer to the [SliverAnimatedList]'s state with a global key:
1046///
1047/// ```dart
1048/// // (e.g. in a stateful widget)
1049/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
1050///
1051/// // ...
1052///
1053/// @override
1054/// Widget build(BuildContext context) {
1055/// return AnimatedList(
1056/// key: listKey,
1057/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
1058/// return const Placeholder();
1059/// },
1060/// );
1061/// }
1062///
1063/// // ...
1064///
1065/// void _updateList() {
1066/// // adds "123" to the AnimatedList
1067/// listKey.currentState!.insertItem(123);
1068/// }
1069/// ```
1070///
1071/// [SliverAnimatedList] item input handlers can also refer to their
1072/// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method.
1073class SliverAnimatedListState extends _SliverAnimatedMultiBoxAdaptorState<SliverAnimatedList> {
1074 @protected
1075 @override
1076 Widget build(BuildContext context) {
1077 return SliverList(delegate: _createDelegate());
1078 }
1079}
1080
1081/// A [SliverGrid] that animates items when they are inserted or removed.
1082///
1083/// This widget's [SliverAnimatedGridState] can be used to dynamically insert or
1084/// remove items. To refer to the [SliverAnimatedGridState] either provide a
1085/// [GlobalKey] or use the static [SliverAnimatedGrid.of] method from an item's
1086/// input callback.
1087///
1088/// {@tool dartpad}
1089/// This sample application uses a [SliverAnimatedGrid] to create an animated
1090/// effect when items are removed or added to the grid.
1091///
1092/// ** See code in examples/api/lib/widgets/animated_grid/sliver_animated_grid.0.dart **
1093/// {@end-tool}
1094///
1095/// See also:
1096///
1097/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
1098/// they are inserted into or removed from a grid.
1099/// * [SliverGrid], which does not animate items when they are inserted or
1100/// removed from a grid.
1101/// * [SliverList], which displays a non-animated list of items.
1102/// * [SliverAnimatedList], which animates items added and removed from a list
1103/// instead of a grid.
1104class SliverAnimatedGrid extends _SliverAnimatedMultiBoxAdaptor {
1105 /// Creates a [SliverGrid] that animates items when they are inserted or
1106 /// removed.
1107 const SliverAnimatedGrid({
1108 super.key,
1109 required super.itemBuilder,
1110 required this.gridDelegate,
1111 super.findChildIndexCallback,
1112 super.initialItemCount = 0,
1113 }) : assert(initialItemCount >= 0);
1114
1115 @override
1116 SliverAnimatedGridState createState() => SliverAnimatedGridState();
1117
1118 /// {@macro flutter.widgets.AnimatedGrid.gridDelegate}
1119 final SliverGridDelegate gridDelegate;
1120
1121 /// The state from the closest instance of this class that encloses the given
1122 /// context.
1123 ///
1124 /// This method is typically used by [SliverAnimatedGrid] item widgets that
1125 /// insert or remove items in response to user input.
1126 ///
1127 /// If no [SliverAnimatedGrid] surrounds the context given, then this function
1128 /// will assert in debug mode and throw an exception in release mode.
1129 ///
1130 /// This method can be expensive (it walks the element tree).
1131 ///
1132 /// See also:
1133 ///
1134 /// * [maybeOf], a similar function that will return null if no
1135 /// [SliverAnimatedGrid] ancestor is found.
1136 static SliverAnimatedGridState of(BuildContext context) {
1137 final SliverAnimatedGridState? result = context
1138 .findAncestorStateOfType<SliverAnimatedGridState>();
1139 assert(() {
1140 if (result == null) {
1141 throw FlutterError(
1142 'SliverAnimatedGrid.of() called with a context that does not contain a SliverAnimatedGrid.\n'
1143 'No SliverAnimatedGridState ancestor could be found starting from the '
1144 'context that was passed to SliverAnimatedGridState.of(). This can '
1145 'happen when the context provided is from the same StatefulWidget that '
1146 'built the AnimatedGrid. Please see the SliverAnimatedGrid documentation '
1147 'for examples of how to refer to an AnimatedGridState object: '
1148 'https://api.flutter.dev/flutter/widgets/SliverAnimatedGridState-class.html\n'
1149 'The context used was:\n'
1150 ' $context',
1151 );
1152 }
1153 return true;
1154 }());
1155 return result!;
1156 }
1157
1158 /// The state from the closest instance of this class that encloses the given
1159 /// context.
1160 ///
1161 /// This method is typically used by [SliverAnimatedGrid] item widgets that
1162 /// insert or remove items in response to user input.
1163 ///
1164 /// If no [SliverAnimatedGrid] surrounds the context given, then this function
1165 /// will return null.
1166 ///
1167 /// This method can be expensive (it walks the element tree).
1168 ///
1169 /// See also:
1170 ///
1171 /// * [of], a similar function that will throw if no [SliverAnimatedGrid]
1172 /// ancestor is found.
1173 static SliverAnimatedGridState? maybeOf(BuildContext context) {
1174 return context.findAncestorStateOfType<SliverAnimatedGridState>();
1175 }
1176}
1177
1178/// The state for a [SliverAnimatedGrid] that animates items when they are
1179/// inserted or removed.
1180///
1181/// When an item is inserted with [insertItem] an animation begins running. The
1182/// animation is passed to [SliverAnimatedGrid.itemBuilder] whenever the item's
1183/// widget is needed.
1184///
1185/// When an item is removed with [removeItem] its animation is reversed.
1186/// The removed item's animation is passed to the [removeItem] builder
1187/// parameter.
1188///
1189/// An app that needs to insert or remove items in response to an event
1190/// can refer to the [SliverAnimatedGrid]'s state with a global key:
1191///
1192/// ```dart
1193/// // (e.g. in a stateful widget)
1194/// GlobalKey<AnimatedGridState> gridKey = GlobalKey<AnimatedGridState>();
1195///
1196/// // ...
1197///
1198/// @override
1199/// Widget build(BuildContext context) {
1200/// return AnimatedGrid(
1201/// key: gridKey,
1202/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
1203/// return const Placeholder();
1204/// },
1205/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 100.0),
1206/// );
1207/// }
1208///
1209/// // ...
1210///
1211/// void _updateGrid() {
1212/// // adds "123" to the AnimatedGrid
1213/// gridKey.currentState!.insertItem(123);
1214/// }
1215/// ```
1216///
1217/// [SliverAnimatedGrid] item input handlers can also refer to their
1218/// [SliverAnimatedGridState] with the static [SliverAnimatedGrid.of] method.
1219class SliverAnimatedGridState extends _SliverAnimatedMultiBoxAdaptorState<SliverAnimatedGrid> {
1220 @protected
1221 @override
1222 Widget build(BuildContext context) {
1223 return SliverGrid(gridDelegate: widget.gridDelegate, delegate: _createDelegate());
1224 }
1225}
1226
1227abstract class _SliverAnimatedMultiBoxAdaptor extends StatefulWidget {
1228 /// Creates a sliver that animates items when they are inserted or removed.
1229 const _SliverAnimatedMultiBoxAdaptor({
1230 super.key,
1231 required this.itemBuilder,
1232 this.findChildIndexCallback,
1233 this.initialItemCount = 0,
1234 }) : assert(initialItemCount >= 0);
1235
1236 /// {@macro flutter.widgets.AnimatedScrollView.itemBuilder}
1237 final AnimatedItemBuilder itemBuilder;
1238
1239 /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
1240 final ChildIndexGetter? findChildIndexCallback;
1241
1242 /// {@macro flutter.widgets.AnimatedScrollView.initialItemCount}
1243 final int initialItemCount;
1244}
1245
1246abstract class _SliverAnimatedMultiBoxAdaptorState<T extends _SliverAnimatedMultiBoxAdaptor>
1247 extends State<T>
1248 with TickerProviderStateMixin {
1249 @override
1250 void initState() {
1251 super.initState();
1252 _itemsCount = widget.initialItemCount;
1253 }
1254
1255 @override
1256 void dispose() {
1257 for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
1258 item.controller!.dispose();
1259 }
1260 super.dispose();
1261 }
1262
1263 final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
1264 final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
1265 int _itemsCount = 0;
1266
1267 _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
1268 final int i = binarySearch(items, _ActiveItem.index(itemIndex));
1269 return i == -1 ? null : items.removeAt(i);
1270 }
1271
1272 _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) {
1273 final int i = binarySearch(items, _ActiveItem.index(itemIndex));
1274 return i == -1 ? null : items[i];
1275 }
1276
1277 // The insertItem() and removeItem() index parameters are defined as if the
1278 // removeItem() operation removed the corresponding list/grid entry
1279 // immediately. The entry is only actually removed from the
1280 // ListView/GridView when the remove animation finishes. The entry is added
1281 // to _outgoingItems when removeItem is called and removed from
1282 // _outgoingItems when the remove animation finishes.
1283
1284 int _indexToItemIndex(int index) {
1285 int itemIndex = index;
1286 for (final _ActiveItem item in _outgoingItems) {
1287 if (item.itemIndex <= itemIndex) {
1288 itemIndex += 1;
1289 } else {
1290 break;
1291 }
1292 }
1293 return itemIndex;
1294 }
1295
1296 int _itemIndexToIndex(int itemIndex) {
1297 int index = itemIndex;
1298 for (final _ActiveItem item in _outgoingItems) {
1299 assert(item.itemIndex != itemIndex);
1300 if (item.itemIndex < itemIndex) {
1301 index -= 1;
1302 } else {
1303 break;
1304 }
1305 }
1306 return index;
1307 }
1308
1309 SliverChildDelegate _createDelegate() {
1310 return SliverChildBuilderDelegate(
1311 _itemBuilder,
1312 childCount: _itemsCount,
1313 findChildIndexCallback: widget.findChildIndexCallback == null
1314 ? null
1315 : (Key key) {
1316 final int? index = widget.findChildIndexCallback!(key);
1317 return index != null ? _indexToItemIndex(index) : null;
1318 },
1319 );
1320 }
1321
1322 Widget _itemBuilder(BuildContext context, int itemIndex) {
1323 final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
1324 if (outgoingItem != null) {
1325 return outgoingItem.removedItemBuilder!(context, outgoingItem.controller!.view);
1326 }
1327
1328 final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, itemIndex);
1329 final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
1330 return widget.itemBuilder(context, _itemIndexToIndex(itemIndex), animation);
1331 }
1332
1333 /// Insert an item at [index] and start an animation that will be passed to
1334 /// [SliverAnimatedGrid.itemBuilder] or [SliverAnimatedList.itemBuilder] when
1335 /// the item is visible.
1336 ///
1337 /// This method's semantics are the same as Dart's [List.insert] method: it
1338 /// increases the length of the list of items by one and shifts
1339 /// all items at or after [index] towards the end of the list of items.
1340 void insertItem(int index, {Duration duration = _kDuration}) {
1341 assert(index >= 0);
1342
1343 final int itemIndex = _indexToItemIndex(index);
1344 assert(itemIndex >= 0 && itemIndex <= _itemsCount);
1345
1346 // Increment the incoming and outgoing item indices to account
1347 // for the insertion.
1348 for (final _ActiveItem item in _incomingItems) {
1349 if (item.itemIndex >= itemIndex) {
1350 item.itemIndex += 1;
1351 }
1352 }
1353 for (final _ActiveItem item in _outgoingItems) {
1354 if (item.itemIndex >= itemIndex) {
1355 item.itemIndex += 1;
1356 }
1357 }
1358
1359 final AnimationController controller = AnimationController(duration: duration, vsync: this);
1360 final _ActiveItem incomingItem = _ActiveItem.incoming(controller, itemIndex);
1361 setState(() {
1362 _incomingItems
1363 ..add(incomingItem)
1364 ..sort();
1365 _itemsCount += 1;
1366 });
1367
1368 controller.forward().then<void>((_) {
1369 _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)!.controller!.dispose();
1370 });
1371 }
1372
1373 /// Insert multiple items at [index] and start an animation that will be passed
1374 /// to [AnimatedGrid.itemBuilder] or [AnimatedList.itemBuilder] when the items
1375 /// are visible.
1376 void insertAllItems(int index, int length, {Duration duration = _kDuration}) {
1377 for (int i = 0; i < length; i++) {
1378 insertItem(index + i, duration: duration);
1379 }
1380 }
1381
1382 /// Remove the item at [index] and start an animation that will be passed
1383 /// to [builder] when the item is visible.
1384 ///
1385 /// Items are removed immediately. After an item has been removed, its index
1386 /// will no longer be passed to the subclass' [SliverAnimatedGrid.itemBuilder]
1387 /// or [SliverAnimatedList.itemBuilder]. However the item will still appear
1388 /// for [duration], and during that time [builder] must construct its widget
1389 /// as needed.
1390 ///
1391 /// This method's semantics are the same as Dart's [List.remove] method: it
1392 /// decreases the length of items by one and shifts
1393 /// all items at or before [index] towards the beginning of the list of items.
1394 void removeItem(int index, AnimatedRemovedItemBuilder builder, {Duration duration = _kDuration}) {
1395 assert(index >= 0);
1396
1397 final int itemIndex = _indexToItemIndex(index);
1398 assert(itemIndex >= 0 && itemIndex < _itemsCount);
1399 assert(_activeItemAt(_outgoingItems, itemIndex) == null);
1400
1401 final _ActiveItem? incomingItem = _removeActiveItemAt(_incomingItems, itemIndex);
1402 final AnimationController controller =
1403 incomingItem?.controller ??
1404 AnimationController(duration: duration, value: 1.0, vsync: this);
1405 final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
1406 setState(() {
1407 _outgoingItems
1408 ..add(outgoingItem)
1409 ..sort();
1410 });
1411
1412 controller.reverse().then<void>((void value) {
1413 _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!.controller!.dispose();
1414
1415 // Decrement the incoming and outgoing item indices to account
1416 // for the removal.
1417 for (final _ActiveItem item in _incomingItems) {
1418 if (item.itemIndex > outgoingItem.itemIndex) {
1419 item.itemIndex -= 1;
1420 }
1421 }
1422 for (final _ActiveItem item in _outgoingItems) {
1423 if (item.itemIndex > outgoingItem.itemIndex) {
1424 item.itemIndex -= 1;
1425 }
1426 }
1427
1428 setState(() => _itemsCount -= 1);
1429 });
1430 }
1431
1432 /// Remove all the items and start an animation that will be passed to
1433 /// `builder` when the items are visible.
1434 ///
1435 /// Items are removed immediately. However, the
1436 /// items will still appear for `duration` and during that time
1437 /// `builder` must construct its widget as needed.
1438 ///
1439 /// This method's semantics are the same as Dart's [List.clear] method: it
1440 /// removes all the items in the list.
1441 void removeAllItems(AnimatedRemovedItemBuilder builder, {Duration duration = _kDuration}) {
1442 for (int i = _itemsCount - 1; i >= 0; i--) {
1443 removeItem(i, builder, duration: duration);
1444 }
1445 }
1446}
1447