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` | 
| ------- | ------- 
| ![Screen Recording 2024-04-03 at 13 01
19](https://github.com/flutter/flutter/assets/12887926/b092e229-5fc6-40a4-acf0-1197ef04c0fb)
| ![Screen Recording 2024-04-03 at 13 01
19](https://github.com/flutter/flutter/assets/12887926/561da112-c1ef-48f1-b043-3947f1f0012e)
|




## 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:
yim
2024-12-06 09:39:06 +08:00
committed by GitHub
parent 9a04500c7f
commit 48c6d0703f
4 changed files with 158 additions and 2 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {