Added boundary feature to ReorderableList. (#146182)
Fixes: #146112 <details open><summary>Code sample</summary> ```dart import 'package:flutter/material.dart'; void main() { runApp(MaterialApp( home: Scaffold( body: Center( child: Container( width: 200, color: Colors.green, child: DragBoundary( child: CustomScrollView( shrinkWrap: true, slivers: <Widget>[ SliverReorderableList( itemBuilder: (BuildContext context, int index) { return ReorderableDragStartListener( key: ValueKey<int>(index), index: index, child: Text('$index'), ); }, itemCount: 5, onReorder: (int fromIndex, int toIndex) {}, ), ], ), ), ), ), ), )); } ``` </details> | without `DragBoundary` | with the `DragBoundary` | | ------- | ------- |  |  | ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [Data Driven Fixes]: https://github.com/flutter/flutter/wiki/Data-driven-Fixes
This commit is contained in:
@@ -102,6 +102,7 @@ class ReorderableListView extends StatefulWidget {
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.autoScrollerVelocityScalar,
|
||||
this.dragBoundaryProvider,
|
||||
}) : assert(
|
||||
(itemExtent == null && prototypeItem == null) ||
|
||||
(itemExtent == null && itemExtentBuilder == null) ||
|
||||
@@ -171,6 +172,7 @@ class ReorderableListView extends StatefulWidget {
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.autoScrollerVelocityScalar,
|
||||
this.dragBoundaryProvider,
|
||||
}) : assert(itemCount >= 0),
|
||||
assert(
|
||||
(itemExtent == null && prototypeItem == null) ||
|
||||
@@ -292,6 +294,9 @@ class ReorderableListView extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.SliverReorderableList.autoScrollerVelocityScalar.default}
|
||||
final double? autoScrollerVelocityScalar;
|
||||
|
||||
/// {@macro flutter.widgets.reorderable_list.dragBoundaryProvider}
|
||||
final ReorderDragBoundaryProvider? dragBoundaryProvider;
|
||||
|
||||
@override
|
||||
State<ReorderableListView> createState() => _ReorderableListViewState();
|
||||
}
|
||||
@@ -446,6 +451,7 @@ class _ReorderableListViewState extends State<ReorderableListView> {
|
||||
onReorderEnd: widget.onReorderEnd,
|
||||
proxyDecorator: widget.proxyDecorator ?? _proxyDecorator,
|
||||
autoScrollerVelocityScalar: widget.autoScrollerVelocityScalar,
|
||||
dragBoundaryProvider: widget.dragBoundaryProvider,
|
||||
),
|
||||
),
|
||||
if (widget.footer != null)
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'debug.dart';
|
||||
import 'drag_boundary.dart';
|
||||
import 'framework.dart';
|
||||
import 'inherited_theme.dart';
|
||||
import 'localizations.dart';
|
||||
@@ -83,6 +84,37 @@ typedef ReorderCallback = void Function(int oldIndex, int newIndex);
|
||||
/// The returned value will typically be the [child] wrapped in other widgets.
|
||||
typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation<double> animation);
|
||||
|
||||
/// Used to provide drag boundaries during drag-and-drop reordering.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// ```dart
|
||||
/// DragBoundary(
|
||||
/// child: CustomScrollView(
|
||||
/// slivers: <Widget>[
|
||||
/// SliverReorderableList(
|
||||
/// itemBuilder: (BuildContext context, int index) {
|
||||
/// return ReorderableDragStartListener(
|
||||
/// key: ValueKey<int>(index),
|
||||
/// index: index,
|
||||
/// child: Text('$index'),
|
||||
/// );
|
||||
/// },
|
||||
/// dragBoundaryProvider: (BuildContext context) {
|
||||
/// return DragBoundary.forRectOf(context);
|
||||
/// },
|
||||
/// itemCount: 5,
|
||||
/// onReorder: (int fromIndex, int toIndex) {},
|
||||
/// ),
|
||||
/// ],
|
||||
/// )
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
/// * [DragBoundary], a widget that provides drag boundaries.
|
||||
typedef ReorderDragBoundaryProvider = DragBoundaryDelegate<Rect>? Function(BuildContext context);
|
||||
|
||||
/// A scrolling container that allows the user to interactively reorder the
|
||||
/// list items.
|
||||
///
|
||||
@@ -139,6 +171,7 @@ class ReorderableList extends StatefulWidget {
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.autoScrollerVelocityScalar,
|
||||
this.dragBoundaryProvider,
|
||||
}) : assert(itemCount >= 0),
|
||||
assert(
|
||||
(itemExtent == null && prototypeItem == null) ||
|
||||
@@ -271,6 +304,14 @@ class ReorderableList extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.SliverReorderableList.autoScrollerVelocityScalar.default}
|
||||
final double? autoScrollerVelocityScalar;
|
||||
|
||||
/// {@template flutter.widgets.reorderable_list.dragBoundaryProvider}
|
||||
/// A callback used to provide drag boundaries during drag-and-drop reordering.
|
||||
///
|
||||
/// If null, the delegate returned by `DragBoundary.forRectMaybeOf` will be used.
|
||||
/// Defaults to null.
|
||||
/// {@endtemplate}
|
||||
final ReorderDragBoundaryProvider? dragBoundaryProvider;
|
||||
|
||||
/// The state from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
@@ -419,6 +460,7 @@ class ReorderableListState extends State<ReorderableList> {
|
||||
onReorderEnd: widget.onReorderEnd,
|
||||
proxyDecorator: widget.proxyDecorator,
|
||||
autoScrollerVelocityScalar: widget.autoScrollerVelocityScalar,
|
||||
dragBoundaryProvider: widget.dragBoundaryProvider,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -465,6 +507,7 @@ class SliverReorderableList extends StatefulWidget {
|
||||
this.itemExtentBuilder,
|
||||
this.prototypeItem,
|
||||
this.proxyDecorator,
|
||||
this.dragBoundaryProvider,
|
||||
double? autoScrollerVelocityScalar,
|
||||
}) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar,
|
||||
assert(itemCount >= 0),
|
||||
@@ -515,6 +558,9 @@ class SliverReorderableList extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
final double autoScrollerVelocityScalar;
|
||||
|
||||
/// {@macro flutter.widgets.reorderable_list.dragBoundaryProvider}
|
||||
final ReorderDragBoundaryProvider? dragBoundaryProvider;
|
||||
|
||||
@override
|
||||
SliverReorderableListState createState() => SliverReorderableListState();
|
||||
|
||||
@@ -1348,9 +1394,15 @@ class _DragInfo extends Drag {
|
||||
index = item.index;
|
||||
child = item.widget.child;
|
||||
capturedThemes = item.widget.capturedThemes;
|
||||
dragPosition = initialPosition;
|
||||
dragOffset = itemRenderBox.globalToLocal(initialPosition);
|
||||
itemSize = item.context.size!;
|
||||
_rawDragPosition = initialPosition;
|
||||
if (listState.widget.dragBoundaryProvider != null) {
|
||||
boundary = listState.widget.dragBoundaryProvider!.call(listState.context);
|
||||
} else {
|
||||
boundary = DragBoundary.forRectMaybeOf(listState.context);
|
||||
}
|
||||
dragPosition = _adjustedDragOffset(initialPosition);
|
||||
itemExtent = _sizeExtent(itemSize, scrollDirection);
|
||||
itemLayoutConstraints = itemRenderBox.constraints;
|
||||
scrollable = Scrollable.of(item.context);
|
||||
@@ -1364,6 +1416,7 @@ class _DragInfo extends Drag {
|
||||
final ReorderItemProxyDecorator? proxyDecorator;
|
||||
final TickerProvider tickerProvider;
|
||||
|
||||
late DragBoundaryDelegate<Rect>? boundary;
|
||||
late SliverReorderableListState listState;
|
||||
late int index;
|
||||
late Widget child;
|
||||
@@ -1375,6 +1428,7 @@ class _DragInfo extends Drag {
|
||||
late CapturedThemes capturedThemes;
|
||||
ScrollableState? scrollable;
|
||||
AnimationController? _proxyAnimation;
|
||||
late Offset _rawDragPosition;
|
||||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
@@ -1399,7 +1453,8 @@ class _DragInfo extends Drag {
|
||||
@override
|
||||
void update(DragUpdateDetails details) {
|
||||
final Offset delta = _restrictAxis(details.delta, scrollDirection);
|
||||
dragPosition += delta;
|
||||
_rawDragPosition += delta;
|
||||
dragPosition = _adjustedDragOffset(_rawDragPosition);
|
||||
onUpdate?.call(this, dragPosition, details.delta);
|
||||
}
|
||||
|
||||
@@ -1416,6 +1471,16 @@ class _DragInfo extends Drag {
|
||||
onCancel?.call(this);
|
||||
}
|
||||
|
||||
Offset _adjustedDragOffset(Offset offset) {
|
||||
if (boundary == null) {
|
||||
return offset;
|
||||
}
|
||||
final Offset adjOffset = boundary!.nearestPositionWithinBoundary(
|
||||
(offset - dragOffset) & itemSize,
|
||||
).shift(dragOffset).topLeft;
|
||||
return adjOffset;
|
||||
}
|
||||
|
||||
void _dropCompleted() {
|
||||
_proxyAnimation?.dispose();
|
||||
_proxyAnimation = null;
|
||||
|
||||
@@ -1925,6 +1925,46 @@ void main() {
|
||||
expect(offsetForFastScroller / offsetForSlowScroller, fastVelocityScalar / slowVelocityScalar);
|
||||
});
|
||||
|
||||
testWidgets('DragBoundary defines the boundary for ReorderableListView.', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Container(
|
||||
margin: const EdgeInsets.only(top: 100),
|
||||
height: 300,
|
||||
child: DragBoundary(
|
||||
child: ReorderableListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ReorderableDragStartListener(
|
||||
key: ValueKey<int>(index),
|
||||
index: index,
|
||||
child: Text('$index'),
|
||||
);
|
||||
},
|
||||
itemCount: 5,
|
||||
onReorder: (int fromIndex, int toIndex) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
TestGesture drag = await tester.startGesture(tester.getCenter(find.text('0')));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await drag.moveBy(const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.text('0')), const Offset(0, 100));
|
||||
await drag.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
drag = await tester.startGesture(tester.getCenter(find.text('0')));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await drag.moveBy(const Offset(0, 800));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getBottomLeft(find.text('0')), const Offset(0, 400));
|
||||
await drag.up();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> longPressDrag(WidgetTester tester, Offset start, Offset end) async {
|
||||
|
||||
@@ -1677,6 +1677,51 @@ void main() {
|
||||
await drag.up();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('DragBoundary defines the boundary for ReorderableList.', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Container(
|
||||
margin: const EdgeInsets.only(top: 100),
|
||||
height: 300,
|
||||
child: DragBoundary(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverReorderableList(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ReorderableDragStartListener(
|
||||
key: ValueKey<int>(index),
|
||||
index: index,
|
||||
child: Text('$index'),
|
||||
);
|
||||
},
|
||||
itemCount: 5,
|
||||
onReorder: (int fromIndex, int toIndex) {},
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
TestGesture drag = await tester.startGesture(tester.getCenter(find.text('0')));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await drag.moveBy(const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.text('0')), const Offset(0, 100));
|
||||
await drag.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
drag = await tester.startGesture(tester.getCenter(find.text('0')));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await drag.moveBy(const Offset(0, 800));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getBottomLeft(find.text('0')), const Offset(0, 400));
|
||||
await drag.up();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
|
||||
class TestList extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user