animated_list.dart 37.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hans Muller's avatar
Hans Muller committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
Hans Muller's avatar
Hans Muller committed
6 7 8 9 10 11

import 'basic.dart';
import 'framework.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scroll_view.dart';
12
import 'sliver.dart';
Hans Muller's avatar
Hans Muller committed
13 14 15
import 'ticker_provider.dart';

/// Signature for the builder callback used by [AnimatedList].
16
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
Hans Muller's avatar
Hans Muller committed
17

18
/// Signature for the builder callback used by [AnimatedListState.removeItem].
19
typedef AnimatedListRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation);
Hans Muller's avatar
Hans Muller committed
20 21

// The default insert/remove animation duration.
22
const Duration _kDuration = Duration(milliseconds: 300);
Hans Muller's avatar
Hans Muller committed
23 24 25 26 27

// Incoming and outgoing AnimatedList items.
class _ActiveItem implements Comparable<_ActiveItem> {
  _ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null;
  _ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder);
28 29 30
  _ActiveItem.index(this.itemIndex)
    : controller = null,
      removedItemBuilder = null;
Hans Muller's avatar
Hans Muller committed
31

32 33
  final AnimationController? controller;
  final AnimatedListRemovedItemBuilder? removedItemBuilder;
Hans Muller's avatar
Hans Muller committed
34 35 36 37 38 39 40 41
  int itemIndex;

  @override
  int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
}

/// A scrolling container that animates items when they are inserted or removed.
///
42 43 44
/// This widget's [AnimatedListState] can be used to dynamically insert or
/// remove items. To refer to the [AnimatedListState] either provide a
/// [GlobalKey] or use the static [of] method from an item's input callback.
Hans Muller's avatar
Hans Muller committed
45 46
///
/// This widget is similar to one created by [ListView.builder].
47 48
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8}
49
///
50
/// {@tool dartpad --template=freeform}
51 52 53 54 55 56 57 58 59
/// This sample application uses an [AnimatedList] to create an effect when
/// items are removed or added to the list.
///
/// ```dart imports
/// import 'package:flutter/foundation.dart';
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
60 61 62 63
/// void main() {
///   runApp(const AnimatedListSample());
/// }
///
64
/// class AnimatedListSample extends StatefulWidget {
65 66
///   const AnimatedListSample({Key? key}) : super(key: key);
///
67
///   @override
68
///   State<AnimatedListSample> createState() => _AnimatedListSampleState();
69 70 71 72
/// }
///
/// class _AnimatedListSampleState extends State<AnimatedListSample> {
///   final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
73 74 75
///   late ListModel<int> _list;
///   int? _selectedItem;
///   late int _nextItem; // The next item inserted when the user presses the '+' button.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
///
///   @override
///   void initState() {
///     super.initState();
///     _list = ListModel<int>(
///       listKey: _listKey,
///       initialItems: <int>[0, 1, 2],
///       removedItemBuilder: _buildRemovedItem,
///     );
///     _nextItem = 3;
///   }
///
///   // Used to build list items that haven't been removed.
///   Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
///     return CardItem(
///       animation: animation,
///       item: _list[index],
///       selected: _selectedItem == _list[index],
///       onTap: () {
///         setState(() {
///           _selectedItem = _selectedItem == _list[index] ? null : _list[index];
///         });
///       },
///     );
///   }
///
///   // Used to build an item after it has been removed from the list. This
///   // method is needed because a removed item remains visible until its
///   // animation has completed (even though it's gone as far this ListModel is
///   // concerned). The widget will be used by the
///   // [AnimatedListState.removeItem] method's
///   // [AnimatedListRemovedItemBuilder] parameter.
///   Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
///     return CardItem(
///       animation: animation,
///       item: item,
///       selected: false,
///       // No gesture detector here: we don't want removed items to be interactive.
///     );
///   }
///
///   // Insert the "next item" into the list model.
///   void _insert() {
119
///     final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
120 121 122 123 124 125
///     _list.insert(index, _nextItem++);
///   }
///
///   // Remove the selected item from the list model.
///   void _remove() {
///     if (_selectedItem != null) {
126
///       _list.removeAt(_list.indexOf(_selectedItem!));
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
///       setState(() {
///         _selectedItem = null;
///       });
///     }
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return MaterialApp(
///       home: Scaffold(
///         appBar: AppBar(
///           title: const Text('AnimatedList'),
///           actions: <Widget>[
///             IconButton(
///               icon: const Icon(Icons.add_circle),
///               onPressed: _insert,
///               tooltip: 'insert a new item',
///             ),
///             IconButton(
///               icon: const Icon(Icons.remove_circle),
///               onPressed: _remove,
///               tooltip: 'remove the selected item',
///             ),
///           ],
///         ),
///         body: Padding(
///           padding: const EdgeInsets.all(16.0),
///           child: AnimatedList(
///             key: _listKey,
///             initialItemCount: _list.length,
///             itemBuilder: _buildItem,
///           ),
///         ),
///       ),
///     );
///   }
/// }
///
165
/// typedef RemovedItemBuilder<T> = Widget Function(T item, BuildContext context, Animation<double> animation);
166
///
167 168 169 170 171 172 173 174 175 176 177
/// /// Keeps a Dart [List] in sync with an [AnimatedList].
/// ///
/// /// The [insert] and [removeAt] methods apply to both the internal list and
/// /// the animated list that belongs to [listKey].
/// ///
/// /// This class only exposes as much of the Dart List API as is needed by the
/// /// sample app. More list methods are easily added, however methods that
/// /// mutate the list must make the same changes to the animated list in terms
/// /// of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
/// class ListModel<E> {
///   ListModel({
178 179 180 181
///     required this.listKey,
///     required this.removedItemBuilder,
///     Iterable<E>? initialItems,
///   }) : _items = List<E>.from(initialItems ?? <E>[]);
182 183
///
///   final GlobalKey<AnimatedListState> listKey;
184
///   final RemovedItemBuilder<E> removedItemBuilder;
185 186
///   final List<E> _items;
///
187
///   AnimatedListState? get _animatedList => listKey.currentState;
188 189 190
///
///   void insert(int index, E item) {
///     _items.insert(index, item);
191
///     _animatedList!.insertItem(index);
192 193 194 195 196
///   }
///
///   E removeAt(int index) {
///     final E removedItem = _items.removeAt(index);
///     if (removedItem != null) {
197
///       _animatedList!.removeItem(
198
///         index,
199
///         (BuildContext context, Animation<double> animation) {
200
///           return removedItemBuilder(removedItem, context, animation);
201
///         },
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
///       );
///     }
///     return removedItem;
///   }
///
///   int get length => _items.length;
///
///   E operator [](int index) => _items[index];
///
///   int indexOf(E item) => _items.indexOf(item);
/// }
///
/// /// Displays its integer item as 'item N' on a Card whose color is based on
/// /// the item's value.
/// ///
/// /// The text is displayed in bright green if [selected] is
/// /// true. This widget's height is based on the [animation] parameter, it
/// /// varies from 0 to 128 as the animation varies from 0.0 to 1.0.
/// class CardItem extends StatelessWidget {
///   const CardItem({
222
///     Key? key,
223
///     this.onTap,
224 225 226 227
///     this.selected = false,
///     required this.animation,
///     required this.item,
///   }) : assert(item >= 0),
228 229 230
///        super(key: key);
///
///   final Animation<double> animation;
231
///   final VoidCallback? onTap;
232 233 234 235 236
///   final int item;
///   final bool selected;
///
///   @override
///   Widget build(BuildContext context) {
237
///     TextStyle textStyle = Theme.of(context).textTheme.headline4!;
238
///     if (selected) {
239
///       textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
240
///     }
241 242 243 244 245 246 247 248 249
///     return Padding(
///       padding: const EdgeInsets.all(2.0),
///       child: SizeTransition(
///         axis: Axis.vertical,
///         sizeFactor: animation,
///         child: GestureDetector(
///           behavior: HitTestBehavior.opaque,
///           onTap: onTap,
///           child: SizedBox(
250
///             height: 80.0,
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
///             child: Card(
///               color: Colors.primaries[item % Colors.primaries.length],
///               child: Center(
///                 child: Text('Item $item', style: textStyle),
///               ),
///             ),
///           ),
///         ),
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
268 269
///  * [SliverAnimatedList], a sliver that animates items when they are inserted
///    or removed from a list.
Hans Muller's avatar
Hans Muller committed
270
class AnimatedList extends StatefulWidget {
271 272
  /// Creates a scrolling container that animates items when they are inserted
  /// or removed.
273
  const AnimatedList({
274 275
    Key? key,
    required this.itemBuilder,
276 277 278
    this.initialItemCount = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
Hans Muller's avatar
Hans Muller committed
279 280 281
    this.controller,
    this.primary,
    this.physics,
282
    this.shrinkWrap = false,
Hans Muller's avatar
Hans Muller committed
283
    this.padding,
284
    this.clipBehavior = Clip.hardEdge,
285 286 287
  }) : assert(itemBuilder != null),
       assert(initialItemCount != null && initialItemCount >= 0),
       super(key: key);
Hans Muller's avatar
Hans Muller committed
288 289 290 291 292 293

  /// Called, as needed, to build list item widgets.
  ///
  /// List items are only built when they're scrolled into view.
  ///
  /// The [AnimatedListItemBuilder] index parameter indicates the item's
294
  /// position in the list. The value of the index parameter will be between 0
295 296 297
  /// and [initialItemCount] plus the total number of items that have been
  /// inserted with [AnimatedListState.insertItem] and less the total number of
  /// items that have been removed with [AnimatedListState.removeItem].
Hans Muller's avatar
Hans Muller committed
298
  ///
299 300
  /// Implementations of this callback should assume that
  /// [AnimatedListState.removeItem] removes an item immediately.
Hans Muller's avatar
Hans Muller committed
301 302
  final AnimatedListItemBuilder itemBuilder;

303
  /// {@template flutter.widgets.animatedList.initialItemCount}
Hans Muller's avatar
Hans Muller committed
304 305
  /// The number of items the list will start with.
  ///
306 307
  /// The appearance of the initial items is not animated. They
  /// are created, as needed, by [itemBuilder] with an animation parameter
Hans Muller's avatar
Hans Muller committed
308
  /// of [kAlwaysCompleteAnimation].
309
  /// {@endtemplate}
Hans Muller's avatar
Hans Muller committed
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
  final int initialItemCount;

  /// The axis along which the scroll view scrolls.
  ///
  /// Defaults to [Axis.vertical].
  final Axis scrollDirection;

  /// Whether the scroll view scrolls in the reading direction.
  ///
  /// For example, if the reading direction is left-to-right and
  /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
  /// left to right when [reverse] is false and from right to left when
  /// [reverse] is true.
  ///
  /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
  /// scrolls from top to bottom when [reverse] is false and from bottom to top
  /// when [reverse] is true.
  ///
  /// Defaults to false.
  final bool reverse;

  /// An object that can be used to control the position to which this scroll
  /// view is scrolled.
  ///
  /// Must be null if [primary] is true.
335 336 337 338 339 340 341 342
  ///
  /// A [ScrollController] serves several purposes. It can be used to control
  /// the initial scroll position (see [ScrollController.initialScrollOffset]).
  /// It can be used to control whether the scroll view should automatically
  /// save and restore its scroll position in the [PageStorage] (see
  /// [ScrollController.keepScrollOffset]). It can be used to read the current
  /// scroll position (see [ScrollController.offset]), or change it (see
  /// [ScrollController.animateTo]).
343
  final ScrollController? controller;
Hans Muller's avatar
Hans Muller committed
344 345 346 347 348 349 350 351 352

  /// Whether this is the primary scroll view associated with the parent
  /// [PrimaryScrollController].
  ///
  /// On iOS, this identifies the scroll view that will scroll to top in
  /// response to a tap in the status bar.
  ///
  /// Defaults to true when [scrollDirection] is [Axis.vertical] and
  /// [controller] is null.
353
  final bool? primary;
Hans Muller's avatar
Hans Muller committed
354 355 356 357 358 359 360

  /// How the scroll view should respond to user input.
  ///
  /// For example, determines how the scroll view continues to animate after the
  /// user stops dragging the scroll view.
  ///
  /// Defaults to matching platform conventions.
361
  final ScrollPhysics? physics;
Hans Muller's avatar
Hans Muller committed
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379

  /// Whether the extent of the scroll view in the [scrollDirection] should be
  /// determined by the contents being viewed.
  ///
  /// If the scroll view does not shrink wrap, then the scroll view will expand
  /// to the maximum allowed size in the [scrollDirection]. If the scroll view
  /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
  /// be true.
  ///
  /// Shrink wrapping the content of the scroll view is significantly more
  /// expensive than expanding to the maximum allowed size because the content
  /// can expand and contract during scrolling, which means the size of the
  /// scroll view needs to be recomputed whenever the scroll position changes.
  ///
  /// Defaults to false.
  final bool shrinkWrap;

  /// The amount of space by which to inset the children.
380
  final EdgeInsetsGeometry? padding;
Hans Muller's avatar
Hans Muller committed
381

382 383 384 385 386
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

387 388
  /// The state from the closest instance of this class that encloses the given
  /// context.
Hans Muller's avatar
Hans Muller committed
389
  ///
390 391
  /// This method is typically used by [AnimatedList] item widgets that insert
  /// or remove items in response to user input.
Hans Muller's avatar
Hans Muller committed
392
  ///
393 394 395
  /// If no [AnimatedList] surrounds the context given, then this function will
  /// assert in debug mode and throw an exception in release mode.
  ///
396 397
  /// This method can be expensive (it walks the element tree).
  ///
398 399 400 401 402
  /// See also:
  ///
  ///  * [maybeOf], a similar function that will return null if no
  ///    [AnimatedList] ancestor is found.
  static AnimatedListState of(BuildContext context) {
Hans Muller's avatar
Hans Muller committed
403
    assert(context != null);
404
    final AnimatedListState? result = context.findAncestorStateOfType<AnimatedListState>();
405 406 407
    assert((){
      if (result == null) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
408
          ErrorSummary('AnimatedList.of() called with a context that does not contain an AnimatedList.'),
409
          ErrorDescription(
410 411
            'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().',
          ),
412 413 414
          ErrorHint(
            'This can happen when the context provided is from the same StatefulWidget that '
            'built the AnimatedList. Please see the AnimatedList documentation for examples '
415 416
            'of how to refer to an AnimatedListState object:\n'
            '  https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html',
417
          ),
418
          context.describeElement('The context used was'),
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
        ]);
      }
      return true;
    }());
    return result!;
  }

  /// The state from the closest instance of this class that encloses the given
  /// context.
  ///
  /// This method is typically used by [AnimatedList] item widgets that insert
  /// or remove items in response to user input.
  ///
  /// If no [AnimatedList] surrounds the context given, then this function will
  /// return null.
  ///
435 436
  /// This method can be expensive (it walks the element tree).
  ///
437 438 439 440 441 442 443
  /// See also:
  ///
  ///  * [of], a similar function that will throw if no [AnimatedList] ancestor
  ///    is found.
  static AnimatedListState? maybeOf(BuildContext context) {
    assert(context != null);
    return context.findAncestorStateOfType<AnimatedListState>();
Hans Muller's avatar
Hans Muller committed
444 445 446
  }

  @override
447
  AnimatedListState createState() => AnimatedListState();
Hans Muller's avatar
Hans Muller committed
448 449 450 451 452
}

/// The state for a scrolling container that animates items when they are
/// inserted or removed.
///
453 454
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget
Hans Muller's avatar
Hans Muller committed
455 456 457
/// is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
458
/// The removed item's animation is passed to the [removeItem] builder
Hans Muller's avatar
Hans Muller committed
459 460 461 462 463 464
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [AnimatedList]'s state with a global key:
///
/// ```dart
465
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
Hans Muller's avatar
Hans Muller committed
466
/// ...
467
/// AnimatedList(key: listKey, ...);
Hans Muller's avatar
Hans Muller committed
468 469 470 471
/// ...
/// listKey.currentState.insert(123);
/// ```
///
472 473
/// [AnimatedList] item input handlers can also refer to their [AnimatedListState]
/// with the static [AnimatedList.of] method.
474
class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
475 476 477 478 479 480 481 482 483
  final GlobalKey<SliverAnimatedListState> _sliverAnimatedListKey = GlobalKey();

  /// Insert an item at [index] and start an animation that will be passed
  /// to [AnimatedList.itemBuilder] when the item is visible.
  ///
  /// This method's semantics are the same as Dart's [List.insert] method:
  /// it increases the length of the list by one and shifts all items at or
  /// after [index] towards the end of the list.
  void insertItem(int index, { Duration duration = _kDuration }) {
484
    _sliverAnimatedListKey.currentState!.insertItem(index, duration: duration);
485 486 487 488 489 490 491 492 493 494 495 496 497 498
  }

  /// Remove the item at [index] and start an animation that will be passed
  /// to [builder] when the item is visible.
  ///
  /// Items are removed immediately. After an item has been removed, its index
  /// will no longer be passed to the [AnimatedList.itemBuilder]. However the
  /// item will still appear in the list for [duration] and during that time
  /// [builder] must construct its widget as needed.
  ///
  /// This method's semantics are the same as Dart's [List.remove] method:
  /// it decreases the length of the list by one and shifts all items at or
  /// before [index] towards the beginning of the list.
  void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
499
    _sliverAnimatedListKey.currentState!.removeItem(index, builder, duration: duration);
500 501 502 503 504 505 506 507 508 509 510
  }

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      scrollDirection: widget.scrollDirection,
      reverse: widget.reverse,
      controller: widget.controller,
      primary: widget.primary,
      physics: widget.physics,
      shrinkWrap: widget.shrinkWrap,
511
      clipBehavior: widget.clipBehavior,
512 513
      slivers: <Widget>[
        SliverPadding(
514
          padding: widget.padding ?? EdgeInsets.zero,
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
          sliver: SliverAnimatedList(
            key: _sliverAnimatedListKey,
            itemBuilder: widget.itemBuilder,
            initialItemCount: widget.initialItemCount,
          ),
        ),
      ],
    );
  }
}

/// A sliver that animates items when they are inserted or removed.
///
/// This widget's [SliverAnimatedListState] can be used to dynamically insert or
/// remove items. To refer to the [SliverAnimatedListState] either provide a
/// [GlobalKey] or use the static [SliverAnimatedList.of] method from an item's
/// input callback.
///
533
/// {@tool dartpad --template=freeform}
534 535 536 537 538 539 540 541 542
/// This sample application uses a [SliverAnimatedList] to create an animated
/// effect when items are removed or added to the list.
///
/// ```dart imports
/// import 'package:flutter/foundation.dart';
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
543
/// void main() => runApp(const SliverAnimatedListSample());
544 545
///
/// class SliverAnimatedListSample extends StatefulWidget {
546 547
///   const SliverAnimatedListSample({Key? key}) : super(key: key);
///
548
///   @override
549
///   State<SliverAnimatedListSample>  createState() => _SliverAnimatedListSampleState();
550 551 552 553 554
/// }
///
/// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> {
///   final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
///   final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
555
///   final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
556 557 558
///   late ListModel<int> _list;
///   int? _selectedItem;
///   late int _nextItem; // The next item inserted when the user presses the '+' button.
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
///
///   @override
///   void initState() {
///     super.initState();
///     _list = ListModel<int>(
///       listKey: _listKey,
///       initialItems: <int>[0, 1, 2],
///       removedItemBuilder: _buildRemovedItem,
///     );
///     _nextItem = 3;
///   }
///
///   // Used to build list items that haven't been removed.
///   Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
///     return CardItem(
///       animation: animation,
///       item: _list[index],
///       selected: _selectedItem == _list[index],
///       onTap: () {
///         setState(() {
///           _selectedItem = _selectedItem == _list[index] ? null : _list[index];
///         });
///       },
///     );
///   }
///
///   // Used to build an item after it has been removed from the list. This
///   // method is needed because a removed item remains visible until its
///   // animation has completed (even though it's gone as far this ListModel is
///   // concerned). The widget will be used by the
///   // [AnimatedListState.removeItem] method's
///   // [AnimatedListRemovedItemBuilder] parameter.
///   Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
///     return CardItem(
///       animation: animation,
///       item: item,
///       selected: false,
///     );
///   }
///
///   // Insert the "next item" into the list model.
///   void _insert() {
601
///     final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
602 603 604 605 606 607
///     _list.insert(index, _nextItem++);
///   }
///
///   // Remove the selected item from the list model.
///   void _remove() {
///     if (_selectedItem != null) {
608
///       _list.removeAt(_list.indexOf(_selectedItem!));
609 610 611 612
///       setState(() {
///         _selectedItem = null;
///       });
///     } else {
613
///       _scaffoldMessengerKey.currentState!.showSnackBar(const SnackBar(
614 615 616 617 618 619 620 621 622 623 624
///         content: Text(
///           'Select an item to remove from the list.',
///           style: TextStyle(fontSize: 20),
///         ),
///       ));
///     }
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return MaterialApp(
625
///       scaffoldMessengerKey: _scaffoldMessengerKey,
626 627 628 629 630
///       home: Scaffold(
///         key: _scaffoldKey,
///         body: CustomScrollView(
///           slivers: <Widget>[
///             SliverAppBar(
631
///               title: const Text(
632 633 634
///                 'SliverAnimatedList',
///                 style: TextStyle(fontSize: 30),
///               ),
635
///               expandedHeight: 60,
636 637 638 639 640 641
///               centerTitle: true,
///               backgroundColor: Colors.amber[900],
///               leading: IconButton(
///                 icon: const Icon(Icons.add_circle),
///                 onPressed: _insert,
///                 tooltip: 'Insert a new item.',
642
///                 iconSize: 32,
643
///               ),
644
///               actions: <Widget>[
645 646 647 648
///                 IconButton(
///                   icon: const Icon(Icons.remove_circle),
///                   onPressed: _remove,
///                   tooltip: 'Remove the selected item.',
649
///                   iconSize: 32,
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
///                 ),
///               ],
///             ),
///             SliverAnimatedList(
///               key: _listKey,
///               initialItemCount: _list.length,
///               itemBuilder: _buildItem,
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
///
665 666
/// typedef RemovedItemBuilder = Widget Function(int item, BuildContext context, Animation<double> animation);
///
667 668 669 670 671 672 673 674 675 676 677
/// // Keeps a Dart [List] in sync with an [AnimatedList].
/// //
/// // The [insert] and [removeAt] methods apply to both the internal list and
/// // the animated list that belongs to [listKey].
/// //
/// // This class only exposes as much of the Dart List API as is needed by the
/// // sample app. More list methods are easily added, however methods that
/// // mutate the list must make the same changes to the animated list in terms
/// // of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
/// class ListModel<E> {
///   ListModel({
678 679 680 681
///     required this.listKey,
///     required this.removedItemBuilder,
///     Iterable<E>? initialItems,
///   }) : _items = List<E>.from(initialItems ?? <E>[]);
682 683
///
///   final GlobalKey<SliverAnimatedListState> listKey;
684
///   final RemovedItemBuilder removedItemBuilder;
685 686
///   final List<E> _items;
///
687
///   SliverAnimatedListState get _animatedList => listKey.currentState!;
688 689 690 691 692 693 694 695 696 697 698
///
///   void insert(int index, E item) {
///     _items.insert(index, item);
///     _animatedList.insertItem(index);
///   }
///
///   E removeAt(int index) {
///     final E removedItem = _items.removeAt(index);
///     if (removedItem != null) {
///       _animatedList.removeItem(
///         index,
699
///         (BuildContext context, Animation<double> animation) => removedItemBuilder(index, context, animation),
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
///       );
///     }
///     return removedItem;
///   }
///
///   int get length => _items.length;
///
///   E operator [](int index) => _items[index];
///
///   int indexOf(E item) => _items.indexOf(item);
/// }
///
/// // Displays its integer item as 'Item N' on a Card whose color is based on
/// // the item's value.
/// //
/// // The card turns gray when [selected] is true. This widget's height
/// // is based on the [animation] parameter. It varies as the animation value
/// // transitions from 0.0 to 1.0.
/// class CardItem extends StatelessWidget {
///   const CardItem({
720
///     Key? key,
721 722
///     this.onTap,
///     this.selected = false,
723 724 725
///     required this.animation,
///     required this.item,
///   }) : assert(item >= 0),
726 727 728
///        super(key: key);
///
///   final Animation<double> animation;
729
///   final VoidCallback? onTap;
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
///   final int item;
///   final bool selected;
///
///   @override
///   Widget build(BuildContext context) {
///     return Padding(
///       padding:
///       const EdgeInsets.only(
///         left: 2.0,
///         right: 2.0,
///         top: 2.0,
///         bottom: 0.0,
///       ),
///       child: SizeTransition(
///         axis: Axis.vertical,
///         sizeFactor: animation,
///         child: GestureDetector(
///           onTap: onTap,
///           child: SizedBox(
749
///             height: 80.0,
750 751 752 753 754 755 756
///             child: Card(
///               color: selected
///                 ? Colors.black12
///                 : Colors.primaries[item % Colors.primaries.length],
///               child: Center(
///                 child: Text(
///                   'Item $item',
757
///                   style: Theme.of(context).textTheme.headline4,
758 759 760 761 762 763 764 765 766 767 768 769 770 771
///                 ),
///               ),
///             ),
///           ),
///         ),
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
772 773 774 775
///  * [SliverList], which does not animate items when they are inserted or
///    removed.
///  * [AnimatedList], a non-sliver scrolling container that animates items when
///    they are inserted or removed.
776 777 778
class SliverAnimatedList extends StatefulWidget {
  /// Creates a sliver that animates items when they are inserted or removed.
  const SliverAnimatedList({
779 780
    Key? key,
    required this.itemBuilder,
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
    this.initialItemCount = 0,
  }) : assert(itemBuilder != null),
       assert(initialItemCount != null && initialItemCount >= 0),
       super(key: key);

  /// Called, as needed, to build list item widgets.
  ///
  /// List items are only built when they're scrolled into view.
  ///
  /// The [AnimatedListItemBuilder] index parameter indicates the item's
  /// position in the list. The value of the index parameter will be between 0
  /// and [initialItemCount] plus the total number of items that have been
  /// inserted with [SliverAnimatedListState.insertItem] and less the total
  /// number of items that have been removed with
  /// [SliverAnimatedListState.removeItem].
  ///
  /// Implementations of this callback should assume that
  /// [SliverAnimatedListState.removeItem] removes an item immediately.
  final AnimatedListItemBuilder itemBuilder;

  /// {@macro flutter.widgets.animatedList.initialItemCount}
  final int initialItemCount;

  @override
  SliverAnimatedListState createState() => SliverAnimatedListState();

  /// The state from the closest instance of this class that encloses the given
  /// context.
  ///
  /// This method is typically used by [SliverAnimatedList] item widgets that
  /// insert or remove items in response to user input.
  ///
813 814 815
  /// If no [SliverAnimatedList] surrounds the context given, then this function
  /// will assert in debug mode and throw an exception in release mode.
  ///
816 817
  /// This method can be expensive (it walks the element tree).
  ///
818 819 820 821 822
  /// See also:
  ///
  ///  * [maybeOf], a similar function that will return null if no
  ///    [SliverAnimatedList] ancestor is found.
  static SliverAnimatedListState of(BuildContext context) {
823
    assert(context != null);
824
    final SliverAnimatedListState? result = context.findAncestorStateOfType<SliverAnimatedListState>();
825 826 827 828 829 830 831 832 833
    assert((){
      if (result == null) {
        throw FlutterError(
          'SliverAnimatedList.of() called with a context that does not contain a SliverAnimatedList.\n'
          'No SliverAnimatedListState ancestor could be found starting from the '
          'context that was passed to SliverAnimatedListState.of(). This can '
          'happen when the context provided is from the same StatefulWidget that '
          'built the AnimatedList. Please see the SliverAnimatedList documentation '
          'for examples of how to refer to an AnimatedListState object: '
834
          'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n'
835
          'The context used was:\n'
836 837
          '  $context',
        );
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
      }
      return true;
    }());
    return result!;
  }

  /// The state from the closest instance of this class that encloses the given
  /// context.
  ///
  /// This method is typically used by [SliverAnimatedList] item widgets that
  /// insert or remove items in response to user input.
  ///
  /// If no [SliverAnimatedList] surrounds the context given, then this function
  /// will return null.
  ///
853 854
  /// This method can be expensive (it walks the element tree).
  ///
855 856 857 858 859 860 861
  /// See also:
  ///
  ///  * [of], a similar function that will throw if no [SliverAnimatedList]
  ///    ancestor is found.
  static SliverAnimatedListState? maybeOf(BuildContext context) {
    assert(context != null);
    return context.findAncestorStateOfType<SliverAnimatedListState>();
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
  }
}

/// The state for a sliver that animates items when they are
/// inserted or removed.
///
/// When an item is inserted with [insertItem] an animation begins running. The
/// animation is passed to [SliverAnimatedList.itemBuilder] whenever the item's
/// widget is needed.
///
/// When an item is removed with [removeItem] its animation is reversed.
/// The removed item's animation is passed to the [removeItem] builder
/// parameter.
///
/// An app that needs to insert or remove items in response to an event
/// can refer to the [SliverAnimatedList]'s state with a global key:
///
/// ```dart
/// GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>();
/// ...
/// SliverAnimatedList(key: listKey, ...);
/// ...
/// listKey.currentState.insert(123);
/// ```
///
/// [SliverAnimatedList] item input handlers can also refer to their
/// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method.
class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProviderStateMixin {

Hans Muller's avatar
Hans Muller committed
891 892 893 894 895 896 897 898 899 900 901 902
  final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
  final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
  int _itemsCount = 0;

  @override
  void initState() {
    super.initState();
    _itemsCount = widget.initialItemCount;
  }

  @override
  void dispose() {
903
    for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
904
      item.controller!.dispose();
905
    }
Hans Muller's avatar
Hans Muller committed
906 907 908
    super.dispose();
  }

909
  _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
910
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
911 912 913
    return i == -1 ? null : items.removeAt(i);
  }

914
  _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) {
915
    final int i = binarySearch(items, _ActiveItem.index(itemIndex));
Hans Muller's avatar
Hans Muller committed
916 917 918 919 920 921 922 923 924 925 926
    return i == -1 ? null : items[i];
  }

  // The insertItem() and removeItem() index parameters are defined as if the
  // removeItem() operation removed the corresponding list entry immediately.
  // The entry is only actually removed from the ListView when the remove animation
  // finishes. The entry is added to _outgoingItems when removeItem is called
  // and removed from _outgoingItems when the remove animation finishes.

  int _indexToItemIndex(int index) {
    int itemIndex = index;
927
    for (final _ActiveItem item in _outgoingItems) {
Hans Muller's avatar
Hans Muller committed
928 929 930 931 932 933 934 935 936 937
      if (item.itemIndex <= itemIndex)
        itemIndex += 1;
      else
        break;
    }
    return itemIndex;
  }

  int _itemIndexToIndex(int itemIndex) {
    int index = itemIndex;
938
    for (final _ActiveItem item in _outgoingItems) {
Hans Muller's avatar
Hans Muller committed
939 940 941 942 943 944 945 946 947
      assert(item.itemIndex != itemIndex);
      if (item.itemIndex < itemIndex)
        index -= 1;
      else
        break;
    }
    return index;
  }

948 949 950 951 952 953
  SliverChildDelegate _createDelegate() {
    return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount);
  }

  /// Insert an item at [index] and start an animation that will be passed to
  /// [SliverAnimatedList.itemBuilder] when the item is visible.
Hans Muller's avatar
Hans Muller committed
954 955 956 957
  ///
  /// This method's semantics are the same as Dart's [List.insert] method:
  /// it increases the length of the list by one and shifts all items at or
  /// after [index] towards the end of the list.
958
  void insertItem(int index, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
959 960 961 962 963 964 965 966
    assert(index != null && index >= 0);
    assert(duration != null);

    final int itemIndex = _indexToItemIndex(index);
    assert(itemIndex >= 0 && itemIndex <= _itemsCount);

    // Increment the incoming and outgoing item indices to account
    // for the insertion.
967
    for (final _ActiveItem item in _incomingItems) {
Hans Muller's avatar
Hans Muller committed
968 969 970
      if (item.itemIndex >= itemIndex)
        item.itemIndex += 1;
    }
971
    for (final _ActiveItem item in _outgoingItems) {
Hans Muller's avatar
Hans Muller committed
972 973 974 975
      if (item.itemIndex >= itemIndex)
        item.itemIndex += 1;
    }

976 977 978 979 980 981 982 983
    final AnimationController controller = AnimationController(
      duration: duration,
      vsync: this,
    );
    final _ActiveItem incomingItem = _ActiveItem.incoming(
      controller,
      itemIndex,
    );
Hans Muller's avatar
Hans Muller committed
984 985 986 987 988 989 990
    setState(() {
      _incomingItems
        ..add(incomingItem)
        ..sort();
      _itemsCount += 1;
    });

991
    controller.forward().then<void>((_) {
992
      _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)!.controller!.dispose();
Hans Muller's avatar
Hans Muller committed
993 994 995 996 997 998 999
    });
  }

  /// Remove the item at [index] and start an animation that will be passed
  /// to [builder] when the item is visible.
  ///
  /// Items are removed immediately. After an item has been removed, its index
1000 1001
  /// will no longer be passed to the [SliverAnimatedList.itemBuilder]. However
  /// the item will still appear in the list for [duration] and during that time
1002
  /// [builder] must construct its widget as needed.
Hans Muller's avatar
Hans Muller committed
1003 1004 1005 1006
  ///
  /// This method's semantics are the same as Dart's [List.remove] method:
  /// it decreases the length of the list by one and shifts all items at or
  /// before [index] towards the beginning of the list.
1007
  void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
Hans Muller's avatar
Hans Muller committed
1008 1009 1010 1011 1012 1013 1014 1015
    assert(index != null && index >= 0);
    assert(builder != null);
    assert(duration != null);

    final int itemIndex = _indexToItemIndex(index);
    assert(itemIndex >= 0 && itemIndex < _itemsCount);
    assert(_activeItemAt(_outgoingItems, itemIndex) == null);

1016
    final _ActiveItem? incomingItem = _removeActiveItemAt(_incomingItems, itemIndex);
Hans Muller's avatar
Hans Muller committed
1017
    final AnimationController controller = incomingItem?.controller
1018 1019
      ?? AnimationController(duration: duration, value: 1.0, vsync: this);
    final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
Hans Muller's avatar
Hans Muller committed
1020 1021 1022 1023 1024 1025
    setState(() {
      _outgoingItems
        ..add(outgoingItem)
        ..sort();
    });

1026
    controller.reverse().then<void>((void value) {
1027
      _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!.controller!.dispose();
Hans Muller's avatar
Hans Muller committed
1028 1029 1030

      // Decrement the incoming and outgoing item indices to account
      // for the removal.
1031
      for (final _ActiveItem item in _incomingItems) {
Hans Muller's avatar
Hans Muller committed
1032 1033 1034
        if (item.itemIndex > outgoingItem.itemIndex)
          item.itemIndex -= 1;
      }
1035
      for (final _ActiveItem item in _outgoingItems) {
Hans Muller's avatar
Hans Muller committed
1036 1037 1038 1039
        if (item.itemIndex > outgoingItem.itemIndex)
          item.itemIndex -= 1;
      }

1040
      setState(() => _itemsCount -= 1);
Hans Muller's avatar
Hans Muller committed
1041 1042 1043 1044
    });
  }

  Widget _itemBuilder(BuildContext context, int itemIndex) {
1045
    final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
1046
    if (outgoingItem != null) {
1047
      return outgoingItem.removedItemBuilder!(
1048
        context,
1049
        outgoingItem.controller!.view,
1050 1051
      );
    }
Hans Muller's avatar
Hans Muller committed
1052

1053
    final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, itemIndex);
Hans Muller's avatar
Hans Muller committed
1054
    final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
1055 1056 1057 1058 1059
    return widget.itemBuilder(
      context,
      _itemIndexToIndex(itemIndex),
      animation,
    );
Hans Muller's avatar
Hans Muller committed
1060 1061 1062 1063
  }

  @override
  Widget build(BuildContext context) {
1064 1065
    return SliverList(
      delegate: _createDelegate(),
Hans Muller's avatar
Hans Muller committed
1066 1067 1068
    );
  }
}