From 3319a9c34eed4cf122bf937f251954c00b03ffe5 Mon Sep 17 00:00:00 2001 From: Maatteogekko <54111322+Maatteogekko@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:35:53 +0200 Subject: [PATCH] Separated builder (#26) fixes https://github.com/wwwdata/implicitly_animated_reorderable_list/issues/25 --------- Co-authored-by: matteo.gentile --- example/lib/examples.dart | 2 +- example/lib/util/box.dart | 12 ++- example/lib/util/highlight_text.dart | 16 ++- example/lib/util/languages.dart | 7 +- example/lib/util/util.dart | 2 +- lib/src/custom_sliver_animated_list.dart | 12 ++- lib/src/implicitly_animated_list.dart | 101 ++++++++++++++++-- lib/src/implicitly_animated_list_base.dart | 5 + .../implicitly_animated_reorderable_list.dart | 83 +++++++++++++- ...iver_child_separated_builder_delegate.dart | 43 ++++++++ 10 files changed, 257 insertions(+), 26 deletions(-) create mode 100644 lib/src/util/sliver_child_separated_builder_delegate.dart diff --git a/example/lib/examples.dart b/example/lib/examples.dart index 8dd1645..a5e960a 100644 --- a/example/lib/examples.dart +++ b/example/lib/examples.dart @@ -89,4 +89,4 @@ class Examples extends StatelessWidget { ); } } - */ \ No newline at end of file + */ diff --git a/example/lib/util/box.dart b/example/lib/util/box.dart index d9833bf..6bc78e4 100644 --- a/example/lib/util/box.dart +++ b/example/lib/util/box.dart @@ -86,7 +86,8 @@ class Box extends StatelessWidget { child: child, ); - if (boxShape == BoxShape.circle || (customBorders != null || borderRadius > 0.0)) { + if (boxShape == BoxShape.circle || + (customBorders != null || borderRadius > 0.0)) { content = ClipRRect( borderRadius: br, child: content, @@ -97,14 +98,17 @@ class Box extends StatelessWidget { content = Material( color: Colors.transparent, type: MaterialType.transparency, - shape: circle ? const CircleBorder() : RoundedRectangleBorder(borderRadius: br), + shape: circle + ? const CircleBorder() + : RoundedRectangleBorder(borderRadius: br), child: InkWell( splashColor: splashColor ?? theme.splashColor, highlightColor: theme.highlightColor, hoverColor: theme.hoverColor, focusColor: theme.focusColor, - customBorder: - circle ? const CircleBorder() : RoundedRectangleBorder(borderRadius: br), + customBorder: circle + ? const CircleBorder() + : RoundedRectangleBorder(borderRadius: br), onTap: onTap, onLongPress: onLongPress, onDoubleTap: onDoubleTap, diff --git a/example/lib/util/highlight_text.dart b/example/lib/util/highlight_text.dart index 6a51a1e..745ec8e 100644 --- a/example/lib/util/highlight_text.dart +++ b/example/lib/util/highlight_text.dart @@ -66,7 +66,8 @@ List> getQueryHighlights(String text, String query) { final t = text.toLowerCase(); final q = query.toLowerCase(); - if (t.isEmpty || q.isEmpty || !t.contains(q)) return [Triplet(0, t.length, false)]; + if (t.isEmpty || q.isEmpty || !t.contains(q)) + return [Triplet(0, t.length, false)]; List> idxs = []; @@ -92,12 +93,16 @@ List> getQueryHighlights(String text, String query) { if (idx.first == 0) { result.add(idx); } else { - result..add(Triplet(0, idx.first, false))..add(idx); + result + ..add(Triplet(0, idx.first, false)) + ..add(idx); } } else if (last.second == idx.first) { result.add(idx); } else { - result..add(Triplet(last.second, idx.first, false))..add(idx); + result + ..add(Triplet(last.second, idx.first, false)) + ..add(idx); } if (isLast && idx.second != t.length) { @@ -140,7 +145,10 @@ class Triplet { @override bool operator ==(Object o) { - return o is Triplet && o.first == first && o.second == second && o.third == third; + return o is Triplet && + o.first == first && + o.second == second && + o.third == third; } @override diff --git a/example/lib/util/languages.dart b/example/lib/util/languages.dart index 87bc157..fc77f60 100644 --- a/example/lib/util/languages.dart +++ b/example/lib/util/languages.dart @@ -64,13 +64,16 @@ class Language { }); @override - String toString() => 'Language englishName: $englishName, nativeName: $nativeName'; + String toString() => + 'Language englishName: $englishName, nativeName: $nativeName'; @override bool operator ==(Object o) { if (identical(this, o)) return true; - return o is Language && o.englishName == englishName && o.nativeName == nativeName; + return o is Language && + o.englishName == englishName && + o.nativeName == nativeName; } @override diff --git a/example/lib/util/util.dart b/example/lib/util/util.dart index 26b9a88..7a0a306 100644 --- a/example/lib/util/util.dart +++ b/example/lib/util/util.dart @@ -1,3 +1,3 @@ export 'box.dart'; export 'highlight_text.dart'; -export 'languages.dart'; \ No newline at end of file +export 'languages.dart'; diff --git a/lib/src/custom_sliver_animated_list.dart b/lib/src/custom_sliver_animated_list.dart index 1cc1e48..21428f3 100644 --- a/lib/src/custom_sliver_animated_list.dart +++ b/lib/src/custom_sliver_animated_list.dart @@ -3,12 +3,18 @@ import 'package:flutter/material.dart'; const Duration _kDuration = Duration(milliseconds: 300); +typedef DelegateBuilder = SliverChildBuilderDelegate Function( + NullableIndexedWidgetBuilder builder, + int itemCount, +); + class CustomSliverAnimatedList extends StatefulWidget { /// Creates a sliver that animates items when they are inserted or removed. const CustomSliverAnimatedList({ Key? key, required this.itemBuilder, this.initialItemCount = 0, + this.delegateBuilder, }) : assert(initialItemCount >= 0), super(key: key); @@ -30,6 +36,9 @@ class CustomSliverAnimatedList extends StatefulWidget { /// {@macro flutter.widgets.animatedList.initialItemCount} final int initialItemCount; + /// Builds the delegate to use for the list view. + final DelegateBuilder? delegateBuilder; + @override CustomSliverAnimatedListState createState() => CustomSliverAnimatedListState(); @@ -149,7 +158,8 @@ class CustomSliverAnimatedListState extends State } SliverChildDelegate _createDelegate() { - return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount); + return widget.delegateBuilder?.call(_itemBuilder, _itemsCount) ?? + SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount); } /// Insert an item at [index] and start an animation that will be passed to diff --git a/lib/src/implicitly_animated_list.dart b/lib/src/implicitly_animated_list.dart index 73d460d..5baade1 100644 --- a/lib/src/implicitly_animated_list.dart +++ b/lib/src/implicitly_animated_list.dart @@ -1,4 +1,5 @@ import 'package:animated_list_plus/src/custom_sliver_animated_list.dart'; +import 'package:animated_list_plus/src/util/sliver_child_separated_builder_delegate.dart'; import 'package:flutter/material.dart' hide AnimatedItemBuilder; import 'src.dart'; @@ -13,6 +14,10 @@ class ImplicitlyAnimatedList extends StatelessWidget { /// List items are only built when they're scrolled into view. final AnimatedItemBuilder itemBuilder; + /// Called to build widgets that get placed between + /// itemBuilder(context, index) and itemBuilder(context, index + 1). + final NullableIndexedWidgetBuilder? separatorBuilder; + /// An optional builder when an item was removed from the list. /// /// If not specified, the [ImplicitlyAnimatedList] uses the [itemBuilder] with @@ -126,6 +131,7 @@ class ImplicitlyAnimatedList extends StatelessWidget { required this.items, required this.itemBuilder, required this.areItemsTheSame, + this.separatorBuilder, this.removeItemBuilder, this.updateItemBuilder, this.insertDuration = const Duration(milliseconds: 500), @@ -143,6 +149,8 @@ class ImplicitlyAnimatedList extends StatelessWidget { @override Widget build(BuildContext context) { + final separatorBuilder = this.separatorBuilder; + return CustomScrollView( scrollDirection: scrollDirection, reverse: reverse, @@ -153,17 +161,30 @@ class ImplicitlyAnimatedList extends StatelessWidget { slivers: [ SliverPadding( padding: padding ?? const EdgeInsets.all(0), - sliver: SliverImplicitlyAnimatedList( - items: items, - itemBuilder: itemBuilder, - areItemsTheSame: areItemsTheSame, - updateItemBuilder: updateItemBuilder, - removeItemBuilder: removeItemBuilder, - insertDuration: insertDuration, - removeDuration: removeDuration, - updateDuration: updateDuration, - spawnIsolate: spawnIsolate, - ), + sliver: separatorBuilder == null + ? SliverImplicitlyAnimatedList( + items: items, + itemBuilder: itemBuilder, + areItemsTheSame: areItemsTheSame, + updateItemBuilder: updateItemBuilder, + removeItemBuilder: removeItemBuilder, + insertDuration: insertDuration, + removeDuration: removeDuration, + updateDuration: updateDuration, + spawnIsolate: spawnIsolate, + ) + : SliverImplicitlyAnimatedList.separated( + items: items, + itemBuilder: itemBuilder, + separatorBuilder: separatorBuilder, + areItemsTheSame: areItemsTheSame, + updateItemBuilder: updateItemBuilder, + removeItemBuilder: removeItemBuilder, + insertDuration: insertDuration, + removeDuration: removeDuration, + updateDuration: updateDuration, + spawnIsolate: spawnIsolate, + ), ), ], ); @@ -210,6 +231,63 @@ class SliverImplicitlyAnimatedList key: key, items: items, itemBuilder: itemBuilder, + delegateBuilder: null, + areItemsTheSame: areItemsTheSame, + removeItemBuilder: removeItemBuilder, + updateItemBuilder: updateItemBuilder, + insertDuration: insertDuration, + removeDuration: removeDuration, + updateDuration: updateDuration, + spawnIsolate: spawnIsolate, + ); + + /// Creates a Flutter Sliver that implicitly animates between the changes of two lists. + /// + /// {@template implicitly_animated_reorderable_list.constructor} + /// The [items] parameter represents the current items that should be displayed in + /// the list. + /// + /// The [itemBuilder] callback is used to build each child as needed. The parent must + /// be a [Reorderable] widget. + /// + /// The [separatorBuilder] is the widget that gets placed between + /// itemBuilder(context, index) and itemBuilder(context, index + 1). + /// + /// The [areItemsTheSame] callback is called by the DiffUtil to decide whether two objects + /// represent the same item. For example, if your items have unique ids, this method should + /// check their id equality. + /// + /// The [onReorderFinished] callback is called in response to when the dragged item has + /// been released and animated to its final destination. Here you should update + /// the underlying data in your model/bloc/database etc. + /// + /// The [spawnIsolate] flag indicates whether to spawn a new isolate on which to + /// calculate the diff between the lists. Usually you wont have to specify this + /// value as the MyersDiff implementation will use its own metrics to decide, whether + /// a new isolate has to be spawned or not for optimal performance. + /// {@endtemplate} + SliverImplicitlyAnimatedList.separated({ + Key? key, + required List items, + required AnimatedItemBuilder itemBuilder, + required ItemDiffUtil areItemsTheSame, + required NullableIndexedWidgetBuilder separatorBuilder, + RemovedItemBuilder? removeItemBuilder, + UpdatedItemBuilder? updateItemBuilder, + Duration insertDuration = const Duration(milliseconds: 500), + Duration removeDuration = const Duration(milliseconds: 500), + Duration updateDuration = const Duration(milliseconds: 500), + bool? spawnIsolate, + }) : super( + key: key, + items: items, + itemBuilder: itemBuilder, + delegateBuilder: (builder, itemCount) => + SliverChildSeparatedBuilderDelegate( + itemBuilder: builder, + separatorBuilder: separatorBuilder, + itemCount: itemCount, + ), areItemsTheSame: areItemsTheSame, removeItemBuilder: removeItemBuilder, updateItemBuilder: updateItemBuilder, @@ -246,6 +324,7 @@ class _SliverImplicitlyAnimatedListState return itemBuilder(context, animation, item, index); } }, + delegateBuilder: widget.delegateBuilder, ); } } diff --git a/lib/src/implicitly_animated_list_base.dart b/lib/src/implicitly_animated_list_base.dart index 5d812a4..66d2bb4 100644 --- a/lib/src/implicitly_animated_list_base.dart +++ b/lib/src/implicitly_animated_list_base.dart @@ -24,6 +24,10 @@ abstract class ImplicitlyAnimatedListBase /// List items are only built when they're scrolled into view. final AnimatedItemBuilder itemBuilder; + /// Called to build widgets that get placed between + /// itemBuilder(context, index) and itemBuilder(context, index + 1). + final DelegateBuilder? delegateBuilder; + /// An optional builder when an item was removed from the list. /// /// If not specified, the [ImplicitlyAnimatedList] uses the [itemBuilder] with @@ -67,6 +71,7 @@ abstract class ImplicitlyAnimatedListBase required this.items, required this.areItemsTheSame, required this.itemBuilder, + required this.delegateBuilder, required this.removeItemBuilder, required this.updateItemBuilder, required this.insertDuration, diff --git a/lib/src/implicitly_animated_reorderable_list.dart b/lib/src/implicitly_animated_reorderable_list.dart index 45ed8a6..9bedba4 100644 --- a/lib/src/implicitly_animated_reorderable_list.dart +++ b/lib/src/implicitly_animated_reorderable_list.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:animated_list_plus/src/custom_sliver_animated_list.dart'; +import 'package:animated_list_plus/src/util/sliver_child_separated_builder_delegate.dart'; import 'package:flutter/material.dart' hide AnimatedItemBuilder; import 'src.dart'; @@ -182,6 +183,83 @@ class ImplicitlyAnimatedReorderableList key: key, items: items, itemBuilder: itemBuilder, + delegateBuilder: null, + areItemsTheSame: areItemsTheSame, + removeItemBuilder: removeItemBuilder, + updateItemBuilder: updateItemBuilder, + insertDuration: insertDuration, + removeDuration: removeDuration, + updateDuration: updateDuration, + spawnIsolate: spawnIsolate, + ); + + /// Creates a Flutter ListView that implicitly animates between the changes of two lists with + /// the support to reorder its items. + /// + /// The [items] parameter represents the current items that should be displayed in + /// the list. + /// + /// The [itemBuilder] callback is used to build each child as needed. The parent must + /// be a [Reorderable] widget. + /// + /// The [separatorBuilder] is the widget that gets placed between + /// itemBuilder(context, index) and itemBuilder(context, index + 1). + /// + /// + /// The [areItemsTheSame] callback is called by the DiffUtil to decide whether two objects + /// represent the same item. For example, if your items have unique ids, this method should + /// check their id equality. + /// + /// The [onReorderFinished] callback is called in response to when the dragged item has + /// been released and animated to its final destination. Here you should update + /// the underlying data in your model/bloc/database etc. + /// + /// The [spawnIsolate] flag indicates whether to spawn a new isolate on which to + /// calculate the diff between the lists. Usually you wont have to specify this + /// value as the MyersDiff implementation will use its own metrics to decide, whether + /// a new isolate has to be spawned or not for optimal performance. + ImplicitlyAnimatedReorderableList.separated({ + Key? key, + required List items, + required AnimatedItemBuilder itemBuilder, + required ItemDiffUtil areItemsTheSame, + required NullableIndexedWidgetBuilder separatorBuilder, + RemovedItemBuilder? removeItemBuilder, + UpdatedItemBuilder? updateItemBuilder, + Duration insertDuration = const Duration(milliseconds: 500), + Duration removeDuration = const Duration(milliseconds: 500), + Duration updateDuration = const Duration(milliseconds: 500), + Duration? liftDuration, + Duration? settleDuration, + bool? spawnIsolate, + this.reverse = false, + this.scrollDirection = Axis.vertical, + this.controller, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.reorderDuration = const Duration(milliseconds: 300), + this.onReorderStarted, + required this.onReorderFinished, + this.header, + this.footer, + }) : liftDuration = liftDuration ?? reorderDuration, + settleDuration = settleDuration ?? liftDuration ?? reorderDuration, + assert( + reorderDuration <= const Duration(milliseconds: 1500), + 'The drag duration should not be longer than 1500 milliseconds.', + ), + super( + key: key, + items: items, + itemBuilder: itemBuilder, + delegateBuilder: (builder, itemCount) => + SliverChildSeparatedBuilderDelegate( + itemBuilder: builder, + separatorBuilder: separatorBuilder, + itemCount: itemCount, + ), areItemsTheSame: areItemsTheSame, removeItemBuilder: removeItemBuilder, updateItemBuilder: updateItemBuilder, @@ -316,7 +394,7 @@ class ImplicitlyAnimatedReorderableListState if (dragKey == null || dragItem == null) return; // Allow the dragged item to be overscrolled to allow for - // continous scrolling while in drag. + // continuous scrolling while in drag. final overscrollBound = _canScroll && !(hasHeader || hasFooter) ? _dragSize : 0; // Constrain the dragged item to the bounds of the list. @@ -705,7 +783,7 @@ class ImplicitlyAnimatedReorderableListState final size = dragItem?.size; // Determine if the dragged widget should be hidden - // immidiately, or with on frame delay in order to + // immediately, or with on frame delay in order to // avoid item flash. final mustRebuild = _dragWidget == null; @@ -725,6 +803,7 @@ class ImplicitlyAnimatedReorderableListState return child; } }, + delegateBuilder: widget.delegateBuilder, ), ), if (hasFooter) diff --git a/lib/src/util/sliver_child_separated_builder_delegate.dart b/lib/src/util/sliver_child_separated_builder_delegate.dart new file mode 100644 index 0000000..d858fdb --- /dev/null +++ b/lib/src/util/sliver_child_separated_builder_delegate.dart @@ -0,0 +1,43 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; + +class SliverChildSeparatedBuilderDelegate extends SliverChildBuilderDelegate { + SliverChildSeparatedBuilderDelegate({ + required NullableIndexedWidgetBuilder itemBuilder, + ChildIndexGetter? findChildIndexCallback, + required NullableIndexedWidgetBuilder separatorBuilder, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super( + (BuildContext context, int index) { + final int itemIndex = index ~/ 2; + final Widget? widget; + if (index.isEven) { + widget = itemBuilder(context, itemIndex); + } else { + widget = separatorBuilder(context, itemIndex); + // ignore: prefer_asserts_with_message , we use FlutterError + assert(() { + if (widget == null) { + throw FlutterError('separatorBuilder cannot return null.'); + } + + return true; + }()); + } + + return widget; + }, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount == null ? null : math.max(0, itemCount * 2 - 1), + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + semanticIndexCallback: (Widget _, int index) { + return index.isEven ? index ~/ 2 : null; + }, + ); +}