Fix a bug with duplicate keys being used in the ReorderableListView. (#74842)
This commit is contained in:
parent
164671e2b1
commit
650b240634
@ -477,14 +477,13 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
// to be inserted.
|
||||
final Map<int, _ReorderableItemState> _items = <int, _ReorderableItemState>{};
|
||||
|
||||
bool _reorderingDrag = false;
|
||||
bool _autoScrolling = false;
|
||||
OverlayEntry? _overlayEntry;
|
||||
_ReorderableItemState? _dragItem;
|
||||
int? _dragIndex;
|
||||
_DragInfo? _dragInfo;
|
||||
int? _insertIndex;
|
||||
Offset? _finalDropPosition;
|
||||
MultiDragGestureRecognizer<MultiDragPointerState>? _recognizer;
|
||||
bool _autoScrolling = false;
|
||||
|
||||
late ScrollableState _scrollable;
|
||||
Axis get _scrollDirection => axisDirectionToAxis(_scrollable.axisDirection);
|
||||
@ -530,11 +529,11 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
}) {
|
||||
assert(0 <= index && index < widget.itemCount);
|
||||
setState(() {
|
||||
if (_reorderingDrag) {
|
||||
if (_dragInfo != null) {
|
||||
cancelReorder();
|
||||
}
|
||||
if (_items.containsKey(index)) {
|
||||
_dragItem = _items[index]!;
|
||||
_dragIndex = index;
|
||||
_recognizer = recognizer
|
||||
..onStart = _dragStart
|
||||
..addPointer(event);
|
||||
@ -561,6 +560,10 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
|
||||
void _registerItem(_ReorderableItemState item) {
|
||||
_items[item.index] = item;
|
||||
if (item.index == _dragInfo?.index) {
|
||||
item.dragging = true;
|
||||
item.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
void _unregisterItem(int index, _ReorderableItemState item) {
|
||||
@ -571,11 +574,12 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
}
|
||||
|
||||
Drag? _dragStart(Offset position) {
|
||||
assert(_reorderingDrag == false);
|
||||
final _ReorderableItemState item = _dragItem!;
|
||||
assert(_dragInfo == null);
|
||||
final _ReorderableItemState item = _items[_dragIndex!]!;
|
||||
item.dragging = true;
|
||||
item.rebuild();
|
||||
|
||||
_insertIndex = item.index;
|
||||
_reorderingDrag = true;
|
||||
_dragInfo = _DragInfo(
|
||||
item: item,
|
||||
initialPosition: position,
|
||||
@ -587,15 +591,13 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
proxyDecorator: widget.proxyDecorator,
|
||||
tickerProvider: this,
|
||||
);
|
||||
_dragInfo!.startDrag();
|
||||
|
||||
final OverlayState overlay = Overlay.of(context)!;
|
||||
assert(_overlayEntry == null);
|
||||
_overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy);
|
||||
overlay.insert(_overlayEntry!);
|
||||
|
||||
_dragInfo!.startDrag();
|
||||
|
||||
item.dragging = true;
|
||||
for (final _ReorderableItemState childItem in _items.values) {
|
||||
if (childItem == item || !childItem.mounted)
|
||||
continue;
|
||||
@ -636,7 +638,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
}
|
||||
|
||||
void _dropCompleted() {
|
||||
final int fromIndex = _dragItem!.index;
|
||||
final int fromIndex = _dragIndex!;
|
||||
final int toIndex = _insertIndex!;
|
||||
if (fromIndex != toIndex) {
|
||||
widget.onReorder.call(fromIndex, toIndex);
|
||||
@ -646,10 +648,13 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
|
||||
void _dragReset() {
|
||||
setState(() {
|
||||
if (_reorderingDrag) {
|
||||
_reorderingDrag = false;
|
||||
_dragItem!.dragging = false;
|
||||
_dragItem = null;
|
||||
if (_dragInfo != null) {
|
||||
if (_dragIndex != null && _items.containsKey(_dragIndex)) {
|
||||
final _ReorderableItemState dragItem = _items[_dragIndex!]!;
|
||||
dragItem._dragging = false;
|
||||
dragItem.rebuild();
|
||||
_dragIndex = null;
|
||||
}
|
||||
_dragInfo?.dispose();
|
||||
_dragInfo = null;
|
||||
_resetItemGap();
|
||||
@ -669,10 +674,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
}
|
||||
|
||||
void _dragUpdateItems() {
|
||||
assert(_reorderingDrag);
|
||||
assert(_dragItem != null);
|
||||
assert(_dragInfo != null);
|
||||
final _ReorderableItemState gapItem = _dragItem!;
|
||||
final double gapExtent = _dragInfo!.itemExtent;
|
||||
final double proxyItemStart = _offsetExtent(_dragInfo!.dragPosition - _dragInfo!.dragOffset, _scrollDirection);
|
||||
final double proxyItemEnd = proxyItemStart + gapExtent;
|
||||
@ -680,7 +682,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
// Find the new index for inserting the item being dragged.
|
||||
int newIndex = _insertIndex!;
|
||||
for (final _ReorderableItemState item in _items.values) {
|
||||
if (item == gapItem || !item.mounted)
|
||||
if (item.index == _dragIndex! || !item.mounted)
|
||||
continue;
|
||||
|
||||
final Rect geometry = item.targetGeometry();
|
||||
@ -735,7 +737,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
if (newIndex != _insertIndex) {
|
||||
_insertIndex = newIndex;
|
||||
for (final _ReorderableItemState item in _items.values) {
|
||||
if (item == gapItem || !item.mounted)
|
||||
if (item.index == _dragIndex! || !item.mounted)
|
||||
continue;
|
||||
item.updateForGap(newIndex, gapExtent, true, _reverse);
|
||||
}
|
||||
@ -784,7 +786,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
curve: Curves.linear
|
||||
);
|
||||
_autoScrolling = false;
|
||||
if (_dragItem != null) {
|
||||
if (_dragInfo != null) {
|
||||
_dragUpdateItems();
|
||||
_autoScrollIfNecessary();
|
||||
}
|
||||
@ -824,7 +826,8 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
// When dragging, the dragged item is still in the list but has been replaced
|
||||
// by a zero height SizedBox, so that the gap can move around. To make the
|
||||
// list extent stable we add a dummy entry to the end.
|
||||
delegate: SliverChildBuilderDelegate(_itemBuilder, childCount: widget.itemCount + (_reorderingDrag ? 1 : 0)),
|
||||
delegate: SliverChildBuilderDelegate(_itemBuilder,
|
||||
childCount: widget.itemCount + (_dragInfo != null ? 1 : 0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1071,7 +1074,7 @@ typedef _DragItemCallback = void Function(_DragInfo item);
|
||||
|
||||
class _DragInfo extends Drag {
|
||||
_DragInfo({
|
||||
required this.item,
|
||||
required _ReorderableItemState item,
|
||||
Offset initialPosition = Offset.zero,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.onUpdate,
|
||||
@ -1082,6 +1085,10 @@ class _DragInfo extends Drag {
|
||||
required this.tickerProvider,
|
||||
}) {
|
||||
final RenderBox itemRenderBox = item.context.findRenderObject()! as RenderBox;
|
||||
listState = item._listState;
|
||||
index = item.index;
|
||||
child = item.widget.child;
|
||||
capturedThemes = item.widget.capturedThemes;
|
||||
dragPosition = initialPosition;
|
||||
dragOffset = itemRenderBox.globalToLocal(initialPosition);
|
||||
itemSize = item.context.size!;
|
||||
@ -1089,7 +1096,6 @@ class _DragInfo extends Drag {
|
||||
scrollable = Scrollable.of(item.context);
|
||||
}
|
||||
|
||||
final _ReorderableItemState item;
|
||||
final Axis scrollDirection;
|
||||
final _DragItemUpdate? onUpdate;
|
||||
final _DragItemCallback? onEnd;
|
||||
@ -1098,10 +1104,14 @@ class _DragInfo extends Drag {
|
||||
final ReorderItemProxyDecorator? proxyDecorator;
|
||||
final TickerProvider tickerProvider;
|
||||
|
||||
late SliverReorderableListState listState;
|
||||
late int index;
|
||||
late Widget child;
|
||||
late Offset dragPosition;
|
||||
late Offset dragOffset;
|
||||
late Size itemSize;
|
||||
late double itemExtent;
|
||||
late CapturedThemes capturedThemes;
|
||||
ScrollableState? scrollable;
|
||||
AnimationController? _proxyAnimation;
|
||||
|
||||
@ -1149,14 +1159,16 @@ class _DragInfo extends Drag {
|
||||
}
|
||||
|
||||
Widget createProxy(BuildContext context) {
|
||||
return item.widget.capturedThemes.wrap(
|
||||
return capturedThemes.wrap(
|
||||
_DragItemProxy(
|
||||
item: item,
|
||||
listState: listState,
|
||||
index: index,
|
||||
child: child,
|
||||
size: itemSize,
|
||||
animation: _proxyAnimation!,
|
||||
position: dragPosition - dragOffset - _overlayOrigin(context),
|
||||
proxyDecorator: proxyDecorator,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1170,14 +1182,18 @@ Offset _overlayOrigin(BuildContext context) {
|
||||
class _DragItemProxy extends StatelessWidget {
|
||||
const _DragItemProxy({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.listState,
|
||||
required this.index,
|
||||
required this.child,
|
||||
required this.position,
|
||||
required this.size,
|
||||
required this.animation,
|
||||
required this.proxyDecorator,
|
||||
}) : super(key: key);
|
||||
|
||||
final _ReorderableItemState item;
|
||||
final SliverReorderableListState listState;
|
||||
final int index;
|
||||
final Widget child;
|
||||
final Offset position;
|
||||
final Size size;
|
||||
final AnimationController animation;
|
||||
@ -1185,15 +1201,14 @@ class _DragItemProxy extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget child = item.widget.child;
|
||||
final Widget proxyChild = proxyDecorator?.call(child, item.index, animation.view) ?? child;
|
||||
final Widget proxyChild = proxyDecorator?.call(child, index, animation.view) ?? child;
|
||||
final Offset overlayOrigin = _overlayOrigin(context);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
Offset effectivePosition = position;
|
||||
final Offset? dropPosition = item._listState._finalDropPosition;
|
||||
final Offset? dropPosition = listState._finalDropPosition;
|
||||
if (dropPosition != null) {
|
||||
effectivePosition = Offset.lerp(dropPosition - overlayOrigin, effectivePosition, Curves.easeOut.transform(animation.value))!;
|
||||
}
|
||||
|
@ -1259,6 +1259,74 @@ void main() {
|
||||
});
|
||||
// TODO(djshuckerow): figure out how to write a test for scrolling the list.
|
||||
});
|
||||
|
||||
testWidgets('ReorderableListView, can deal with the dragged item getting unmounted and rebuilt during drag', (WidgetTester tester) async {
|
||||
// See https://github.com/flutter/flutter/issues/74840 for more details.
|
||||
final List<int> items = List<int>.generate(100, (int index) => index);
|
||||
|
||||
void handleReorder(int fromIndex, int toIndex) {
|
||||
if (toIndex > fromIndex) {
|
||||
toIndex -= 1;
|
||||
}
|
||||
items.insert(toIndex, items.removeAt(fromIndex));
|
||||
}
|
||||
|
||||
// The list is 800x600, 8 items, each item is 800x100 with
|
||||
// an "item $index" text widget at the item's origin. Drags are initiated by
|
||||
// a simple press on the text widget.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: ReorderableListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
key: ValueKey<int>(items[index]),
|
||||
height: 100,
|
||||
child: ReorderableDragStartListener(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('item ${items[index]}'),
|
||||
],
|
||||
),
|
||||
index: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
onReorder: handleReorder,
|
||||
),
|
||||
));
|
||||
|
||||
// Drag item 0 downwards and force an auto scroll off the end of the list
|
||||
// far enough that item zeros original entry in the list is unmounted.
|
||||
final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
|
||||
await tester.pump(kPressTimeout);
|
||||
// Off the bottom of the screen, which should autoscroll until we hit the
|
||||
// end of the list
|
||||
await drag.moveBy(const Offset(0, 700));
|
||||
await tester.pump(const Duration(seconds: 30));
|
||||
await tester.pumpAndSettle();
|
||||
// Ensure we made it to the bottom (only 4 should be showing as there should
|
||||
// be a gap at the end for the drop area of the dragged item.
|
||||
for (final int i in <int>[95, 96, 97, 98, 99]) {
|
||||
expect(find.text('item $i'), findsOneWidget);
|
||||
}
|
||||
|
||||
// Drag back to off the top of the list, which should autoscroll until
|
||||
// we hit the beginning of the list. This should cause the first item's
|
||||
// entry to be rebuilt. However, the contents should not be in both places.
|
||||
await drag.moveBy(const Offset(0, -1400));
|
||||
await tester.pump(const Duration(seconds: 30));
|
||||
await tester.pumpAndSettle();
|
||||
// Release back at the top so item 0 should drop where it was
|
||||
await drag.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should not have changed anything
|
||||
for (final int i in <int>[0, 1, 2, 3, 4, 5]) {
|
||||
expect(find.text('item $i'), findsOneWidget);
|
||||
}
|
||||
expect(items.take(8), orderedEquals(<int>[0, 1, 2, 3, 4, 5, 6, 7]));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> longPressDrag(WidgetTester tester, Offset start, Offset end) async {
|
||||
|
Loading…
x
Reference in New Issue
Block a user