Refactor Animated[List, Grid, SliverList, SliverGrid] to share common code (#113793)
This commit is contained in:
parent
1cfdac4b23
commit
0e70a97e2e
@ -462,9 +462,9 @@ class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
|
|||||||
SliverGridLayout getLayout(SliverConstraints constraints) {
|
SliverGridLayout getLayout(SliverConstraints constraints) {
|
||||||
assert(_debugAssertIsValid(constraints.crossAxisExtent));
|
assert(_debugAssertIsValid(constraints.crossAxisExtent));
|
||||||
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
|
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
|
||||||
// TODO(gspencergoog): Figure out why we need this in release mode (and only
|
// Ensure a minimum count of 1, can be zero and result in an infinite extent
|
||||||
// in release mode). https://github.com/flutter/flutter/issues/113109
|
// below when the window size is 0.
|
||||||
crossAxisCount = crossAxisCount < 1 ? 1 : crossAxisCount;
|
crossAxisCount = math.max(1, crossAxisCount);
|
||||||
final double usableCrossAxisExtent = math.max(
|
final double usableCrossAxisExtent = math.max(
|
||||||
0.0,
|
0.0,
|
||||||
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
|
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
|
||||||
|
@ -1,700 +0,0 @@
|
|||||||
// 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 'animated_grid.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].
|
|
||||||
///
|
|
||||||
/// This is deprecated, use the identical [AnimatedItemBuilder] instead.
|
|
||||||
@Deprecated(
|
|
||||||
'Use AnimatedItemBuilder instead. '
|
|
||||||
'This feature was deprecated after v3.5.0-4.0.pre.',
|
|
||||||
)
|
|
||||||
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation<double> animation);
|
|
||||||
|
|
||||||
/// Signature for the builder callback used by [AnimatedListState.removeItem].
|
|
||||||
///
|
|
||||||
/// This is deprecated, use the identical [AnimatedRemovedItemBuilder]
|
|
||||||
/// instead.
|
|
||||||
@Deprecated(
|
|
||||||
'Use AnimatedRemovedItemBuilder instead. '
|
|
||||||
'This feature was deprecated after v3.5.0-4.0.pre.',
|
|
||||||
)
|
|
||||||
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 AnimatedRemovedItemBuilder? 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}
|
|
||||||
/// This sample application uses an [AnimatedList] to create an effect when
|
|
||||||
/// items are removed or added to the list.
|
|
||||||
///
|
|
||||||
/// ** See code in examples/api/lib/widgets/animated_list/animated_list.0.dart **
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
|
|
||||||
/// or removed from a list.
|
|
||||||
/// * [SliverAnimatedGrid], a sliver which animates items when they are
|
|
||||||
/// inserted or removed from a grid.
|
|
||||||
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
|
|
||||||
/// they are inserted or removed in a grid.
|
|
||||||
class AnimatedList extends StatefulWidget {
|
|
||||||
/// Creates a scrolling container that animates items when they are inserted
|
|
||||||
/// or removed.
|
|
||||||
const AnimatedList({
|
|
||||||
super.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);
|
|
||||||
|
|
||||||
/// Called, as needed, to build list item widgets.
|
|
||||||
///
|
|
||||||
/// List items are only built when they're scrolled into view.
|
|
||||||
///
|
|
||||||
/// The [AnimatedItemBuilder] 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 AnimatedItemBuilder 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.
|
|
||||||
///
|
|
||||||
/// This method can be expensive (it walks the element tree).
|
|
||||||
///
|
|
||||||
/// 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:\n'
|
|
||||||
' 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.
|
|
||||||
///
|
|
||||||
/// This method can be expensive (it walks the element tree).
|
|
||||||
///
|
|
||||||
/// 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
|
|
||||||
/// // (e.g. in a stateful widget)
|
|
||||||
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
|
||||||
///
|
|
||||||
/// // ...
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// Widget build(BuildContext context) {
|
|
||||||
/// return AnimatedList(
|
|
||||||
/// key: listKey,
|
|
||||||
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
|
||||||
/// return const Placeholder();
|
|
||||||
/// },
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // ...
|
|
||||||
///
|
|
||||||
/// void _updateList() {
|
|
||||||
/// // adds "123" to the AnimatedList
|
|
||||||
/// listKey.currentState!.insertItem(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, AnimatedRemovedItemBuilder 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}
|
|
||||||
/// This sample application uses a [SliverAnimatedList] to create an animated
|
|
||||||
/// effect when items are removed or added to the list.
|
|
||||||
///
|
|
||||||
/// ** See code in examples/api/lib/widgets/animated_list/sliver_animated_list.0.dart **
|
|
||||||
/// {@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.
|
|
||||||
/// * [SliverAnimatedGrid], a sliver which animates items when they are
|
|
||||||
/// inserted into or removed from a grid.
|
|
||||||
/// * [AnimatedGrid], a non-sliver scrolling container that animates items when
|
|
||||||
/// they are inserted into or removed from a grid.
|
|
||||||
class SliverAnimatedList extends StatefulWidget {
|
|
||||||
/// Creates a sliver that animates items when they are inserted or removed.
|
|
||||||
const SliverAnimatedList({
|
|
||||||
super.key,
|
|
||||||
required this.itemBuilder,
|
|
||||||
this.findChildIndexCallback,
|
|
||||||
this.initialItemCount = 0,
|
|
||||||
}) : assert(itemBuilder != null),
|
|
||||||
assert(initialItemCount != null && initialItemCount >= 0);
|
|
||||||
|
|
||||||
/// Called, as needed, to build list item widgets.
|
|
||||||
///
|
|
||||||
/// List items are only built when they're scrolled into view.
|
|
||||||
///
|
|
||||||
/// The [AnimatedItemBuilder] 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 AnimatedItemBuilder itemBuilder;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
|
||||||
final ChildIndexGetter? findChildIndexCallback;
|
|
||||||
|
|
||||||
/// {@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.
|
|
||||||
///
|
|
||||||
/// This method can be expensive (it walks the element tree).
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
///
|
|
||||||
/// This method can be expensive (it walks the element tree).
|
|
||||||
///
|
|
||||||
/// 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
|
|
||||||
/// // (e.g. in a stateful widget)
|
|
||||||
/// GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
|
|
||||||
///
|
|
||||||
/// // ...
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// Widget build(BuildContext context) {
|
|
||||||
/// return AnimatedList(
|
|
||||||
/// key: listKey,
|
|
||||||
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
|
|
||||||
/// return const Placeholder();
|
|
||||||
/// },
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // ...
|
|
||||||
///
|
|
||||||
/// void _updateList() {
|
|
||||||
/// // adds "123" to the AnimatedList
|
|
||||||
/// listKey.currentState!.insertItem(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,
|
|
||||||
findChildIndexCallback: widget.findChildIndexCallback == null
|
|
||||||
? null
|
|
||||||
: (Key key) {
|
|
||||||
final int? index = widget.findChildIndexCallback!(key);
|
|
||||||
return index != null ? _indexToItemIndex(index) : null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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, AnimatedRemovedItemBuilder 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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -19,8 +19,7 @@ export 'foundation.dart' show UniqueKey;
|
|||||||
export 'rendering.dart' show TextSelectionHandleType;
|
export 'rendering.dart' show TextSelectionHandleType;
|
||||||
export 'src/widgets/actions.dart';
|
export 'src/widgets/actions.dart';
|
||||||
export 'src/widgets/animated_cross_fade.dart';
|
export 'src/widgets/animated_cross_fade.dart';
|
||||||
export 'src/widgets/animated_grid.dart';
|
export 'src/widgets/animated_scroll_view.dart';
|
||||||
export 'src/widgets/animated_list.dart';
|
|
||||||
export 'src/widgets/animated_size.dart';
|
export 'src/widgets/animated_size.dart';
|
||||||
export 'src/widgets/animated_switcher.dart';
|
export 'src/widgets/animated_switcher.dart';
|
||||||
export 'src/widgets/annotated_region.dart';
|
export 'src/widgets/annotated_region.dart';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user