| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scroll_view.dart'; |
| import 'sliver.dart'; |
| import 'ticker_provider.dart'; |
| |
| /// Signature for the builder callback used by [AnimatedList]. |
| typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation); |
| |
| /// Signature for the builder callback used by [AnimatedListState.removeItem]. |
| typedef AnimatedListRemovedItemBuilder = Widget Function(BuildContext context, Animation<double> animation); |
| |
| // The default insert/remove animation duration. |
| const Duration _kDuration = Duration(milliseconds: 300); |
| |
| // Incoming and outgoing AnimatedList items. |
| class _ActiveItem implements Comparable<_ActiveItem> { |
| _ActiveItem.incoming(this.controller, this.itemIndex) : removedItemBuilder = null; |
| _ActiveItem.outgoing(this.controller, this.itemIndex, this.removedItemBuilder); |
| _ActiveItem.index(this.itemIndex) |
| : controller = null, |
| removedItemBuilder = null; |
| |
| final AnimationController? controller; |
| final AnimatedListRemovedItemBuilder? removedItemBuilder; |
| int itemIndex; |
| |
| @override |
| int compareTo(_ActiveItem other) => itemIndex - other.itemIndex; |
| } |
| |
| /// A scrolling container that animates items when they are inserted or removed. |
| /// |
| /// This widget's [AnimatedListState] can be used to dynamically insert or |
| /// remove items. To refer to the [AnimatedListState] either provide a |
| /// [GlobalKey] or use the static [of] method from an item's input callback. |
| /// |
| /// This widget is similar to one created by [ListView.builder]. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=ZtfItHwFlZ8} |
| /// |
| /// {@tool dartpad --template=freeform} |
| /// This sample application uses an [AnimatedList] to create an effect when |
| /// items are removed or added to the list. |
| /// |
| /// ```dart imports |
| /// import 'package:flutter/foundation.dart'; |
| /// import 'package:flutter/material.dart'; |
| /// ``` |
| /// |
| /// ```dart |
| /// void main() { |
| /// runApp(const AnimatedListSample()); |
| /// } |
| /// |
| /// class AnimatedListSample extends StatefulWidget { |
| /// const AnimatedListSample({Key? key}) : super(key: key); |
| /// |
| /// @override |
| /// _AnimatedListSampleState createState() => _AnimatedListSampleState(); |
| /// } |
| /// |
| /// class _AnimatedListSampleState extends State<AnimatedListSample> { |
| /// final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>(); |
| /// late ListModel<int> _list; |
| /// int? _selectedItem; |
| /// late int _nextItem; // The next item inserted when the user presses the '+' button. |
| /// |
| /// @override |
| /// void initState() { |
| /// super.initState(); |
| /// _list = ListModel<int>( |
| /// listKey: _listKey, |
| /// initialItems: <int>[0, 1, 2], |
| /// removedItemBuilder: _buildRemovedItem, |
| /// ); |
| /// _nextItem = 3; |
| /// } |
| /// |
| /// // Used to build list items that haven't been removed. |
| /// Widget _buildItem(BuildContext context, int index, Animation<double> animation) { |
| /// return CardItem( |
| /// animation: animation, |
| /// item: _list[index], |
| /// selected: _selectedItem == _list[index], |
| /// onTap: () { |
| /// setState(() { |
| /// _selectedItem = _selectedItem == _list[index] ? null : _list[index]; |
| /// }); |
| /// }, |
| /// ); |
| /// } |
| /// |
| /// // Used to build an item after it has been removed from the list. This |
| /// // method is needed because a removed item remains visible until its |
| /// // animation has completed (even though it's gone as far this ListModel is |
| /// // concerned). The widget will be used by the |
| /// // [AnimatedListState.removeItem] method's |
| /// // [AnimatedListRemovedItemBuilder] parameter. |
| /// Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) { |
| /// return CardItem( |
| /// animation: animation, |
| /// item: item, |
| /// selected: false, |
| /// // No gesture detector here: we don't want removed items to be interactive. |
| /// ); |
| /// } |
| /// |
| /// // Insert the "next item" into the list model. |
| /// void _insert() { |
| /// final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!); |
| /// _list.insert(index, _nextItem++); |
| /// } |
| /// |
| /// // Remove the selected item from the list model. |
| /// void _remove() { |
| /// if (_selectedItem != null) { |
| /// _list.removeAt(_list.indexOf(_selectedItem!)); |
| /// setState(() { |
| /// _selectedItem = null; |
| /// }); |
| /// } |
| /// } |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return MaterialApp( |
| /// home: Scaffold( |
| /// appBar: AppBar( |
| /// title: const Text('AnimatedList'), |
| /// actions: <Widget>[ |
| /// IconButton( |
| /// icon: const Icon(Icons.add_circle), |
| /// onPressed: _insert, |
| /// tooltip: 'insert a new item', |
| /// ), |
| /// IconButton( |
| /// icon: const Icon(Icons.remove_circle), |
| /// onPressed: _remove, |
| /// tooltip: 'remove the selected item', |
| /// ), |
| /// ], |
| /// ), |
| /// body: Padding( |
| /// padding: const EdgeInsets.all(16.0), |
| /// child: AnimatedList( |
| /// key: _listKey, |
| /// initialItemCount: _list.length, |
| /// itemBuilder: _buildItem, |
| /// ), |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// |
| /// typedef RemovedItemBuilder = Widget Function(int item, BuildContext context, Animation<double> animation); |
| /// |
| /// /// Keeps a Dart [List] in sync with an [AnimatedList]. |
| /// /// |
| /// /// The [insert] and [removeAt] methods apply to both the internal list and |
| /// /// the animated list that belongs to [listKey]. |
| /// /// |
| /// /// This class only exposes as much of the Dart List API as is needed by the |
| /// /// sample app. More list methods are easily added, however methods that |
| /// /// mutate the list must make the same changes to the animated list in terms |
| /// /// of [AnimatedListState.insertItem] and [AnimatedList.removeItem]. |
| /// class ListModel<E> { |
| /// ListModel({ |
| /// required this.listKey, |
| /// required this.removedItemBuilder, |
| /// Iterable<E>? initialItems, |
| /// }) : _items = List<E>.from(initialItems ?? <E>[]); |
| /// |
| /// final GlobalKey<AnimatedListState> listKey; |
| /// final RemovedItemBuilder removedItemBuilder; |
| /// final List<E> _items; |
| /// |
| /// AnimatedListState? get _animatedList => listKey.currentState; |
| /// |
| /// void insert(int index, E item) { |
| /// _items.insert(index, item); |
| /// _animatedList!.insertItem(index); |
| /// } |
| /// |
| /// E removeAt(int index) { |
| /// final E removedItem = _items.removeAt(index); |
| /// if (removedItem != null) { |
| /// _animatedList!.removeItem( |
| /// index, |
| /// (BuildContext context, Animation<double> animation) { |
| /// return removedItemBuilder(index, context, animation); |
| /// }, |
| /// ); |
| /// } |
| /// return removedItem; |
| /// } |
| /// |
| /// int get length => _items.length; |
| /// |
| /// E operator [](int index) => _items[index]; |
| /// |
| /// int indexOf(E item) => _items.indexOf(item); |
| /// } |
| /// |
| /// /// Displays its integer item as 'item N' on a Card whose color is based on |
| /// /// the item's value. |
| /// /// |
| /// /// The text is displayed in bright green if [selected] is |
| /// /// true. This widget's height is based on the [animation] parameter, it |
| /// /// varies from 0 to 128 as the animation varies from 0.0 to 1.0. |
| /// class CardItem extends StatelessWidget { |
| /// const CardItem({ |
| /// Key? key, |
| /// this.onTap, |
| /// this.selected = false, |
| /// required this.animation, |
| /// required this.item, |
| /// }) : assert(item >= 0), |
| /// super(key: key); |
| /// |
| /// final Animation<double> animation; |
| /// final VoidCallback? onTap; |
| /// final int item; |
| /// final bool selected; |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// TextStyle textStyle = Theme.of(context).textTheme.headline4!; |
| /// if (selected) |
| /// textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]); |
| /// return Padding( |
| /// padding: const EdgeInsets.all(2.0), |
| /// child: SizeTransition( |
| /// axis: Axis.vertical, |
| /// sizeFactor: animation, |
| /// child: GestureDetector( |
| /// behavior: HitTestBehavior.opaque, |
| /// onTap: onTap, |
| /// child: SizedBox( |
| /// height: 80.0, |
| /// child: Card( |
| /// color: Colors.primaries[item % Colors.primaries.length], |
| /// child: Center( |
| /// child: Text('Item $item', style: textStyle), |
| /// ), |
| /// ), |
| /// ), |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [SliverAnimatedList], a sliver that animates items when they are inserted |
| /// or removed from a list. |
| class AnimatedList extends StatefulWidget { |
| /// Creates a scrolling container that animates items when they are inserted |
| /// or removed. |
| const AnimatedList({ |
| Key? key, |
| required this.itemBuilder, |
| this.initialItemCount = 0, |
| this.scrollDirection = Axis.vertical, |
| this.reverse = false, |
| this.controller, |
| this.primary, |
| this.physics, |
| this.shrinkWrap = false, |
| this.padding, |
| this.clipBehavior = Clip.hardEdge, |
| }) : assert(itemBuilder != null), |
| assert(initialItemCount != null && initialItemCount >= 0), |
| super(key: key); |
| |
| /// Called, as needed, to build list item widgets. |
| /// |
| /// List items are only built when they're scrolled into view. |
| /// |
| /// The [AnimatedListItemBuilder] index parameter indicates the item's |
| /// position in the list. The value of the index parameter will be between 0 |
| /// and [initialItemCount] plus the total number of items that have been |
| /// inserted with [AnimatedListState.insertItem] and less the total number of |
| /// items that have been removed with [AnimatedListState.removeItem]. |
| /// |
| /// Implementations of this callback should assume that |
| /// [AnimatedListState.removeItem] removes an item immediately. |
| final AnimatedListItemBuilder itemBuilder; |
| |
| /// {@template flutter.widgets.animatedList.initialItemCount} |
| /// The number of items the list will start with. |
| /// |
| /// The appearance of the initial items is not animated. They |
| /// are created, as needed, by [itemBuilder] with an animation parameter |
| /// of [kAlwaysCompleteAnimation]. |
| /// {@endtemplate} |
| final int initialItemCount; |
| |
| /// The axis along which the scroll view scrolls. |
| /// |
| /// Defaults to [Axis.vertical]. |
| final Axis scrollDirection; |
| |
| /// Whether the scroll view scrolls in the reading direction. |
| /// |
| /// For example, if the reading direction is left-to-right and |
| /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from |
| /// left to right when [reverse] is false and from right to left when |
| /// [reverse] is true. |
| /// |
| /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view |
| /// scrolls from top to bottom when [reverse] is false and from bottom to top |
| /// when [reverse] is true. |
| /// |
| /// Defaults to false. |
| final bool reverse; |
| |
| /// An object that can be used to control the position to which this scroll |
| /// view is scrolled. |
| /// |
| /// Must be null if [primary] is true. |
| /// |
| /// A [ScrollController] serves several purposes. It can be used to control |
| /// the initial scroll position (see [ScrollController.initialScrollOffset]). |
| /// It can be used to control whether the scroll view should automatically |
| /// save and restore its scroll position in the [PageStorage] (see |
| /// [ScrollController.keepScrollOffset]). It can be used to read the current |
| /// scroll position (see [ScrollController.offset]), or change it (see |
| /// [ScrollController.animateTo]). |
| final ScrollController? controller; |
| |
| /// Whether this is the primary scroll view associated with the parent |
| /// [PrimaryScrollController]. |
| /// |
| /// On iOS, this identifies the scroll view that will scroll to top in |
| /// response to a tap in the status bar. |
| /// |
| /// Defaults to true when [scrollDirection] is [Axis.vertical] and |
| /// [controller] is null. |
| final bool? primary; |
| |
| /// How the scroll view should respond to user input. |
| /// |
| /// For example, determines how the scroll view continues to animate after the |
| /// user stops dragging the scroll view. |
| /// |
| /// Defaults to matching platform conventions. |
| final ScrollPhysics? physics; |
| |
| /// Whether the extent of the scroll view in the [scrollDirection] should be |
| /// determined by the contents being viewed. |
| /// |
| /// If the scroll view does not shrink wrap, then the scroll view will expand |
| /// to the maximum allowed size in the [scrollDirection]. If the scroll view |
| /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must |
| /// be true. |
| /// |
| /// Shrink wrapping the content of the scroll view is significantly more |
| /// expensive than expanding to the maximum allowed size because the content |
| /// can expand and contract during scrolling, which means the size of the |
| /// scroll view needs to be recomputed whenever the scroll position changes. |
| /// |
| /// Defaults to false. |
| final bool shrinkWrap; |
| |
| /// The amount of space by which to inset the children. |
| final EdgeInsetsGeometry? padding; |
| |
| /// {@macro flutter.material.Material.clipBehavior} |
| /// |
| /// Defaults to [Clip.hardEdge]. |
| final Clip clipBehavior; |
| |
| /// 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 |
| /// assert in debug mode and throw an exception in release mode. |
| /// |
| /// See also: |
| /// |
| /// * [maybeOf], a similar function that will return null if no |
| /// [AnimatedList] ancestor is found. |
| static AnimatedListState of(BuildContext context) { |
| assert(context != null); |
| final AnimatedListState? result = context.findAncestorStateOfType<AnimatedListState>(); |
| assert((){ |
| if (result == null) { |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary( |
| 'AnimatedList.of() called with a context that does not contain an AnimatedList.'), |
| ErrorDescription( |
| 'No AnimatedList ancestor could be found starting from the context that was passed to AnimatedList.of().'), |
| ErrorHint( |
| 'This can happen when the context provided is from the same StatefulWidget that ' |
| 'built the AnimatedList. Please see the AnimatedList documentation for examples ' |
| 'of how to refer to an AnimatedListState object:' |
| ' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html' |
| ), |
| context.describeElement('The context used was') |
| ]); |
| } |
| 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. |
| /// |
| /// 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>(); |
| } |
| |
| @override |
| AnimatedListState createState() => AnimatedListState(); |
| } |
| |
| /// The state for a scrolling container that animates items when they are |
| /// inserted or removed. |
| /// |
| /// When an item is inserted with [insertItem] an animation begins running. The |
| /// animation is passed to [AnimatedList.itemBuilder] whenever the item's widget |
| /// is needed. |
| /// |
| /// When an item is removed with [removeItem] its animation is reversed. |
| /// The removed item's animation is passed to the [removeItem] builder |
| /// parameter. |
| /// |
| /// An app that needs to insert or remove items in response to an event |
| /// can refer to the [AnimatedList]'s state with a global key: |
| /// |
| /// ```dart |
| /// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>(); |
| /// ... |
| /// AnimatedList(key: listKey, ...); |
| /// ... |
| /// listKey.currentState.insert(123); |
| /// ``` |
| /// |
| /// [AnimatedList] item input handlers can also refer to their [AnimatedListState] |
| /// with the static [AnimatedList.of] method. |
| class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> { |
| final GlobalKey<SliverAnimatedListState> _sliverAnimatedListKey = GlobalKey(); |
| |
| /// Insert an item at [index] and start an animation that will be passed |
| /// to [AnimatedList.itemBuilder] when the item is visible. |
| /// |
| /// This method's semantics are the same as Dart's [List.insert] method: |
| /// it increases the length of the list by one and shifts all items at or |
| /// after [index] towards the end of the list. |
| void insertItem(int index, { Duration duration = _kDuration }) { |
| _sliverAnimatedListKey.currentState!.insertItem(index, duration: duration); |
| } |
| |
| /// Remove the item at [index] and start an animation that will be passed |
| /// to [builder] when the item is visible. |
| /// |
| /// Items are removed immediately. After an item has been removed, its index |
| /// will no longer be passed to the [AnimatedList.itemBuilder]. However the |
| /// item will still appear in the list for [duration] and during that time |
| /// [builder] must construct its widget as needed. |
| /// |
| /// This method's semantics are the same as Dart's [List.remove] method: |
| /// it decreases the length of the list by one and shifts all items at or |
| /// before [index] towards the beginning of the list. |
| void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) { |
| _sliverAnimatedListKey.currentState!.removeItem(index, builder, duration: duration); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return CustomScrollView( |
| scrollDirection: widget.scrollDirection, |
| reverse: widget.reverse, |
| controller: widget.controller, |
| primary: widget.primary, |
| physics: widget.physics, |
| shrinkWrap: widget.shrinkWrap, |
| clipBehavior: widget.clipBehavior, |
| slivers: <Widget>[ |
| SliverPadding( |
| padding: widget.padding ?? EdgeInsets.zero, |
| sliver: SliverAnimatedList( |
| key: _sliverAnimatedListKey, |
| itemBuilder: widget.itemBuilder, |
| initialItemCount: widget.initialItemCount, |
| ), |
| ), |
| ], |
| ); |
| } |
| } |
| |
| /// A sliver that animates items when they are inserted or removed. |
| /// |
| /// This widget's [SliverAnimatedListState] can be used to dynamically insert or |
| /// remove items. To refer to the [SliverAnimatedListState] either provide a |
| /// [GlobalKey] or use the static [SliverAnimatedList.of] method from an item's |
| /// input callback. |
| /// |
| /// {@tool dartpad --template=freeform} |
| /// This sample application uses a [SliverAnimatedList] to create an animated |
| /// effect when items are removed or added to the list. |
| /// |
| /// ```dart imports |
| /// import 'package:flutter/foundation.dart'; |
| /// import 'package:flutter/material.dart'; |
| /// ``` |
| /// |
| /// ```dart |
| /// void main() => runApp(const SliverAnimatedListSample()); |
| /// |
| /// class SliverAnimatedListSample extends StatefulWidget { |
| /// const SliverAnimatedListSample({Key? key}) : super(key: key); |
| /// |
| /// @override |
| /// _SliverAnimatedListSampleState createState() => _SliverAnimatedListSampleState(); |
| /// } |
| /// |
| /// class _SliverAnimatedListSampleState extends State<SliverAnimatedListSample> { |
| /// final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>(); |
| /// final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); |
| /// final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); |
| /// late ListModel<int> _list; |
| /// int? _selectedItem; |
| /// late int _nextItem; // The next item inserted when the user presses the '+' button. |
| /// |
| /// @override |
| /// void initState() { |
| /// super.initState(); |
| /// _list = ListModel<int>( |
| /// listKey: _listKey, |
| /// initialItems: <int>[0, 1, 2], |
| /// removedItemBuilder: _buildRemovedItem, |
| /// ); |
| /// _nextItem = 3; |
| /// } |
| /// |
| /// // Used to build list items that haven't been removed. |
| /// Widget _buildItem(BuildContext context, int index, Animation<double> animation) { |
| /// return CardItem( |
| /// animation: animation, |
| /// item: _list[index], |
| /// selected: _selectedItem == _list[index], |
| /// onTap: () { |
| /// setState(() { |
| /// _selectedItem = _selectedItem == _list[index] ? null : _list[index]; |
| /// }); |
| /// }, |
| /// ); |
| /// } |
| /// |
| /// // Used to build an item after it has been removed from the list. This |
| /// // method is needed because a removed item remains visible until its |
| /// // animation has completed (even though it's gone as far this ListModel is |
| /// // concerned). The widget will be used by the |
| /// // [AnimatedListState.removeItem] method's |
| /// // [AnimatedListRemovedItemBuilder] parameter. |
| /// Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) { |
| /// return CardItem( |
| /// animation: animation, |
| /// item: item, |
| /// selected: false, |
| /// ); |
| /// } |
| /// |
| /// // Insert the "next item" into the list model. |
| /// void _insert() { |
| /// final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem!); |
| /// _list.insert(index, _nextItem++); |
| /// } |
| /// |
| /// // Remove the selected item from the list model. |
| /// void _remove() { |
| /// if (_selectedItem != null) { |
| /// _list.removeAt(_list.indexOf(_selectedItem!)); |
| /// setState(() { |
| /// _selectedItem = null; |
| /// }); |
| /// } else { |
| /// _scaffoldMessengerKey.currentState!.showSnackBar(const SnackBar( |
| /// content: Text( |
| /// 'Select an item to remove from the list.', |
| /// style: TextStyle(fontSize: 20), |
| /// ), |
| /// )); |
| /// } |
| /// } |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return MaterialApp( |
| /// scaffoldMessengerKey: _scaffoldMessengerKey, |
| /// home: Scaffold( |
| /// key: _scaffoldKey, |
| /// body: CustomScrollView( |
| /// slivers: <Widget>[ |
| /// SliverAppBar( |
| /// title: const Text( |
| /// 'SliverAnimatedList', |
| /// style: TextStyle(fontSize: 30), |
| /// ), |
| /// expandedHeight: 60, |
| /// centerTitle: true, |
| /// backgroundColor: Colors.amber[900], |
| /// leading: IconButton( |
| /// icon: const Icon(Icons.add_circle), |
| /// onPressed: _insert, |
| /// tooltip: 'Insert a new item.', |
| /// iconSize: 32, |
| /// ), |
| /// actions: <Widget>[ |
| /// IconButton( |
| /// icon: const Icon(Icons.remove_circle), |
| /// onPressed: _remove, |
| /// tooltip: 'Remove the selected item.', |
| /// iconSize: 32, |
| /// ), |
| /// ], |
| /// ), |
| /// SliverAnimatedList( |
| /// key: _listKey, |
| /// initialItemCount: _list.length, |
| /// itemBuilder: _buildItem, |
| /// ), |
| /// ], |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// |
| /// typedef RemovedItemBuilder = Widget Function(int item, BuildContext context, Animation<double> animation); |
| /// |
| /// // Keeps a Dart [List] in sync with an [AnimatedList]. |
| /// // |
| /// // The [insert] and [removeAt] methods apply to both the internal list and |
| /// // the animated list that belongs to [listKey]. |
| /// // |
| /// // This class only exposes as much of the Dart List API as is needed by the |
| /// // sample app. More list methods are easily added, however methods that |
| /// // mutate the list must make the same changes to the animated list in terms |
| /// // of [AnimatedListState.insertItem] and [AnimatedList.removeItem]. |
| /// class ListModel<E> { |
| /// ListModel({ |
| /// required this.listKey, |
| /// required this.removedItemBuilder, |
| /// Iterable<E>? initialItems, |
| /// }) : _items = List<E>.from(initialItems ?? <E>[]); |
| /// |
| /// final GlobalKey<SliverAnimatedListState> listKey; |
| /// final RemovedItemBuilder removedItemBuilder; |
| /// final List<E> _items; |
| /// |
| /// SliverAnimatedListState get _animatedList => listKey.currentState!; |
| /// |
| /// void insert(int index, E item) { |
| /// _items.insert(index, item); |
| /// _animatedList.insertItem(index); |
| /// } |
| /// |
| /// E removeAt(int index) { |
| /// final E removedItem = _items.removeAt(index); |
| /// if (removedItem != null) { |
| /// _animatedList.removeItem( |
| /// index, |
| /// (BuildContext context, Animation<double> animation) => removedItemBuilder(index, context, animation), |
| /// ); |
| /// } |
| /// return removedItem; |
| /// } |
| /// |
| /// int get length => _items.length; |
| /// |
| /// E operator [](int index) => _items[index]; |
| /// |
| /// int indexOf(E item) => _items.indexOf(item); |
| /// } |
| /// |
| /// // Displays its integer item as 'Item N' on a Card whose color is based on |
| /// // the item's value. |
| /// // |
| /// // The card turns gray when [selected] is true. This widget's height |
| /// // is based on the [animation] parameter. It varies as the animation value |
| /// // transitions from 0.0 to 1.0. |
| /// class CardItem extends StatelessWidget { |
| /// const CardItem({ |
| /// Key? key, |
| /// this.onTap, |
| /// this.selected = false, |
| /// required this.animation, |
| /// required this.item, |
| /// }) : assert(item >= 0), |
| /// super(key: key); |
| /// |
| /// final Animation<double> animation; |
| /// final VoidCallback? onTap; |
| /// final int item; |
| /// final bool selected; |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return Padding( |
| /// padding: |
| /// const EdgeInsets.only( |
| /// left: 2.0, |
| /// right: 2.0, |
| /// top: 2.0, |
| /// bottom: 0.0, |
| /// ), |
| /// child: SizeTransition( |
| /// axis: Axis.vertical, |
| /// sizeFactor: animation, |
| /// child: GestureDetector( |
| /// onTap: onTap, |
| /// child: SizedBox( |
| /// height: 80.0, |
| /// child: Card( |
| /// color: selected |
| /// ? Colors.black12 |
| /// : Colors.primaries[item % Colors.primaries.length], |
| /// child: Center( |
| /// child: Text( |
| /// 'Item $item', |
| /// style: Theme.of(context).textTheme.headline4, |
| /// ), |
| /// ), |
| /// ), |
| /// ), |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [SliverList], which does not animate items when they are inserted or |
| /// removed. |
| /// * [AnimatedList], a non-sliver scrolling container that animates items when |
| /// they are inserted or removed. |
| class SliverAnimatedList extends StatefulWidget { |
| /// Creates a sliver that animates items when they are inserted or removed. |
| const SliverAnimatedList({ |
| Key? key, |
| required this.itemBuilder, |
| this.initialItemCount = 0, |
| }) : assert(itemBuilder != null), |
| assert(initialItemCount != null && initialItemCount >= 0), |
| super(key: key); |
| |
| /// Called, as needed, to build list item widgets. |
| /// |
| /// List items are only built when they're scrolled into view. |
| /// |
| /// The [AnimatedListItemBuilder] index parameter indicates the item's |
| /// position in the list. The value of the index parameter will be between 0 |
| /// and [initialItemCount] plus the total number of items that have been |
| /// inserted with [SliverAnimatedListState.insertItem] and less the total |
| /// number of items that have been removed with |
| /// [SliverAnimatedListState.removeItem]. |
| /// |
| /// Implementations of this callback should assume that |
| /// [SliverAnimatedListState.removeItem] removes an item immediately. |
| final AnimatedListItemBuilder itemBuilder; |
| |
| /// {@macro flutter.widgets.animatedList.initialItemCount} |
| final int initialItemCount; |
| |
| @override |
| SliverAnimatedListState createState() => SliverAnimatedListState(); |
| |
| /// The state from the closest instance of this class that encloses the given |
| /// context. |
| /// |
| /// This method is typically used by [SliverAnimatedList] item widgets that |
| /// insert or remove items in response to user input. |
| /// |
| /// If no [SliverAnimatedList] surrounds the context given, then this function |
| /// will assert in debug mode and throw an exception in release mode. |
| /// |
| /// See also: |
| /// |
| /// * [maybeOf], a similar function that will return null if no |
| /// [SliverAnimatedList] ancestor is found. |
| static SliverAnimatedListState of(BuildContext context) { |
| assert(context != null); |
| final SliverAnimatedListState? result = context.findAncestorStateOfType<SliverAnimatedListState>(); |
| 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: ' |
| 'https://api.flutter.dev/flutter/widgets/SliverAnimatedListState-class.html\n' |
| 'The context used was:\n' |
| ' $context'); |
| } |
| 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. |
| /// |
| /// 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>(); |
| } |
| } |
| |
| /// The state for a sliver that animates items when they are |
| /// inserted or removed. |
| /// |
| /// When an item is inserted with [insertItem] an animation begins running. The |
| /// animation is passed to [SliverAnimatedList.itemBuilder] whenever the item's |
| /// widget is needed. |
| /// |
| /// When an item is removed with [removeItem] its animation is reversed. |
| /// The removed item's animation is passed to the [removeItem] builder |
| /// parameter. |
| /// |
| /// An app that needs to insert or remove items in response to an event |
| /// can refer to the [SliverAnimatedList]'s state with a global key: |
| /// |
| /// ```dart |
| /// GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); |
| /// ... |
| /// SliverAnimatedList(key: listKey, ...); |
| /// ... |
| /// listKey.currentState.insert(123); |
| /// ``` |
| /// |
| /// [SliverAnimatedList] item input handlers can also refer to their |
| /// [SliverAnimatedListState] with the static [SliverAnimatedList.of] method. |
| class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProviderStateMixin { |
| |
| final List<_ActiveItem> _incomingItems = <_ActiveItem>[]; |
| final List<_ActiveItem> _outgoingItems = <_ActiveItem>[]; |
| int _itemsCount = 0; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _itemsCount = widget.initialItemCount; |
| } |
| |
| @override |
| void dispose() { |
| for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) { |
| item.controller!.dispose(); |
| } |
| super.dispose(); |
| } |
| |
| _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) { |
| final int i = binarySearch(items, _ActiveItem.index(itemIndex)); |
| return i == -1 ? null : items.removeAt(i); |
| } |
| |
| _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) { |
| final int i = binarySearch(items, _ActiveItem.index(itemIndex)); |
| return i == -1 ? null : items[i]; |
| } |
| |
| // The insertItem() and removeItem() index parameters are defined as if the |
| // removeItem() operation removed the corresponding list entry immediately. |
| // The entry is only actually removed from the ListView when the remove animation |
| // finishes. The entry is added to _outgoingItems when removeItem is called |
| // and removed from _outgoingItems when the remove animation finishes. |
| |
| int _indexToItemIndex(int index) { |
| int itemIndex = index; |
| for (final _ActiveItem item in _outgoingItems) { |
| if (item.itemIndex <= itemIndex) |
| itemIndex += 1; |
| else |
| break; |
| } |
| return itemIndex; |
| } |
| |
| int _itemIndexToIndex(int itemIndex) { |
| int index = itemIndex; |
| for (final _ActiveItem item in _outgoingItems) { |
| assert(item.itemIndex != itemIndex); |
| if (item.itemIndex < itemIndex) |
| index -= 1; |
| else |
| break; |
| } |
| return index; |
| } |
| |
| SliverChildDelegate _createDelegate() { |
| return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount); |
| } |
| |
| /// Insert an item at [index] and start an animation that will be passed to |
| /// [SliverAnimatedList.itemBuilder] when the item is visible. |
| /// |
| /// This method's semantics are the same as Dart's [List.insert] method: |
| /// it increases the length of the list by one and shifts all items at or |
| /// after [index] towards the end of the list. |
| void insertItem(int index, { Duration duration = _kDuration }) { |
| assert(index != null && index >= 0); |
| assert(duration != null); |
| |
| final int itemIndex = _indexToItemIndex(index); |
| assert(itemIndex >= 0 && itemIndex <= _itemsCount); |
| |
| // Increment the incoming and outgoing item indices to account |
| // for the insertion. |
| for (final _ActiveItem item in _incomingItems) { |
| if (item.itemIndex >= itemIndex) |
| item.itemIndex += 1; |
| } |
| for (final _ActiveItem item in _outgoingItems) { |
| if (item.itemIndex >= itemIndex) |
| item.itemIndex += 1; |
| } |
| |
| final AnimationController controller = AnimationController( |
| duration: duration, |
| vsync: this, |
| ); |
| final _ActiveItem incomingItem = _ActiveItem.incoming( |
| controller, |
| itemIndex, |
| ); |
| setState(() { |
| _incomingItems |
| ..add(incomingItem) |
| ..sort(); |
| _itemsCount += 1; |
| }); |
| |
| controller.forward().then<void>((_) { |
| _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)!.controller!.dispose(); |
| }); |
| } |
| |
| /// Remove the item at [index] and start an animation that will be passed |
| /// to [builder] when the item is visible. |
| /// |
| /// Items are removed immediately. After an item has been removed, its index |
| /// will no longer be passed to the [SliverAnimatedList.itemBuilder]. However |
| /// the item will still appear in the list for [duration] and during that time |
| /// [builder] must construct its widget as needed. |
| /// |
| /// This method's semantics are the same as Dart's [List.remove] method: |
| /// it decreases the length of the list by one and shifts all items at or |
| /// before [index] towards the beginning of the list. |
| void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) { |
| assert(index != null && index >= 0); |
| assert(builder != null); |
| assert(duration != null); |
| |
| final int itemIndex = _indexToItemIndex(index); |
| assert(itemIndex >= 0 && itemIndex < _itemsCount); |
| assert(_activeItemAt(_outgoingItems, itemIndex) == null); |
| |
| final _ActiveItem? incomingItem = _removeActiveItemAt(_incomingItems, itemIndex); |
| final AnimationController controller = incomingItem?.controller |
| ?? AnimationController(duration: duration, value: 1.0, vsync: this); |
| final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder); |
| setState(() { |
| _outgoingItems |
| ..add(outgoingItem) |
| ..sort(); |
| }); |
| |
| controller.reverse().then<void>((void value) { |
| _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!.controller!.dispose(); |
| |
| // Decrement the incoming and outgoing item indices to account |
| // for the removal. |
| for (final _ActiveItem item in _incomingItems) { |
| if (item.itemIndex > outgoingItem.itemIndex) |
| item.itemIndex -= 1; |
| } |
| for (final _ActiveItem item in _outgoingItems) { |
| if (item.itemIndex > outgoingItem.itemIndex) |
| item.itemIndex -= 1; |
| } |
| |
| setState(() => _itemsCount -= 1); |
| }); |
| } |
| |
| Widget _itemBuilder(BuildContext context, int itemIndex) { |
| final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, itemIndex); |
| if (outgoingItem != null) { |
| return outgoingItem.removedItemBuilder!( |
| context, |
| outgoingItem.controller!.view, |
| ); |
| } |
| |
| final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, itemIndex); |
| final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation; |
| return widget.itemBuilder( |
| context, |
| _itemIndexToIndex(itemIndex), |
| animation, |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return SliverList( |
| delegate: _createDelegate(), |
| ); |
| } |
| } |