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