mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add boundary feature to the drag gesture. (#147521)
Inspired by the review on #146182. This PR adds boundary feature to the drag gestures, including `MultiDragGestureRecognizer` and `DragGestureRecognizer`. The `GestureDetector` widget will also benefit from this.
This commit is contained in:
parent
7ee7fff210
commit
c051b69e2a
@ -0,0 +1,69 @@
|
|||||||
|
// 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/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const DragBoundaryExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragBoundaryExampleApp extends StatefulWidget {
|
||||||
|
const DragBoundaryExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => DragBoundaryExampleAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragBoundaryExampleAppState extends State<DragBoundaryExampleApp> {
|
||||||
|
Offset _currentPosition = Offset.zero;
|
||||||
|
Offset _initialPosition = Offset.zero;
|
||||||
|
final Size _boxSize = const Size(100, 100);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(100),
|
||||||
|
child: DragBoundary(
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: _currentPosition.dy,
|
||||||
|
left: _currentPosition.dx,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onPanStart: (DragStartDetails details) {
|
||||||
|
_initialPosition = details.localPosition - _currentPosition;
|
||||||
|
},
|
||||||
|
onPanUpdate: (DragUpdateDetails details) {
|
||||||
|
_currentPosition = details.localPosition - _initialPosition;
|
||||||
|
final Rect withinBoundary = DragBoundary.forRectOf(context, useGlobalPosition: false).nearestPositionWithinBoundary(
|
||||||
|
_currentPosition & _boxSize,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = withinBoundary.topLeft;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: _boxSize.width,
|
||||||
|
height: _boxSize.height,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
// 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/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/widgets/gesture_detector/gesture_detector.3.dart'
|
||||||
|
as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('The red box always moves inside the green box', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.DragBoundaryExampleApp(),
|
||||||
|
);
|
||||||
|
final Finder greenFinder = find.byType(Container).first;
|
||||||
|
final Finder redFinder = find.byType(Container).last;
|
||||||
|
final TestGesture drag = await tester.startGesture(tester.getCenter(redFinder));
|
||||||
|
await tester.pump(kLongPressTimeout);
|
||||||
|
await drag.moveBy(const Offset(1000, 1000));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getBottomRight(redFinder), tester.getBottomRight(greenFinder));
|
||||||
|
await drag.moveBy(const Offset(-2000, -2000));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getTopLeft(redFinder), tester.getTopLeft(greenFinder));
|
||||||
|
await drag.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
}
|
113
packages/flutter/lib/src/widgets/drag_boundary.dart
Normal file
113
packages/flutter/lib/src/widgets/drag_boundary.dart
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 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 'dart:ui';
|
||||||
|
|
||||||
|
import 'framework.dart';
|
||||||
|
|
||||||
|
/// The interface for defining the algorithm for a boundary that a specified shape is dragged within.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [DragBoundary], an [InheritedWidget] that provides a [DragBoundaryDelegate] to its descendants.
|
||||||
|
///
|
||||||
|
/// `T` is a data class that defines the shape being dragged. For example, when dragging a rectangle within the boundary,
|
||||||
|
/// `T` should be a `Rect`.
|
||||||
|
abstract class DragBoundaryDelegate<T> {
|
||||||
|
/// Returns whether the specified dragged object is within the boundary.
|
||||||
|
bool isWithinBoundary(T draggedObject);
|
||||||
|
|
||||||
|
/// Returns the given dragged object after moving it fully inside
|
||||||
|
/// the boundary with the shortest distance.
|
||||||
|
///
|
||||||
|
/// If the bounds cannot contain the dragged object, an exception is thrown.
|
||||||
|
T nearestPositionWithinBoundary(T draggedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DragBoundaryDelegateForRect extends DragBoundaryDelegate<Rect> {
|
||||||
|
_DragBoundaryDelegateForRect(this.boundary);
|
||||||
|
final Rect? boundary;
|
||||||
|
@override
|
||||||
|
bool isWithinBoundary(Rect draggedObject) {
|
||||||
|
if (boundary == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return boundary!.contains(draggedObject.topLeft) && boundary!.contains(draggedObject.bottomRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect nearestPositionWithinBoundary(Rect draggedObject) {
|
||||||
|
if (boundary == null) {
|
||||||
|
return draggedObject;
|
||||||
|
}
|
||||||
|
if (boundary!.right - draggedObject.width < boundary!.left ||
|
||||||
|
boundary!.bottom - draggedObject.height < boundary!.top) {
|
||||||
|
throw FlutterError(
|
||||||
|
'The rect is larger than the boundary. '
|
||||||
|
'The rect width must be less than the boundary width, and the rect height must be less than the boundary height.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final double left = clampDouble(
|
||||||
|
draggedObject.left,
|
||||||
|
boundary!.left,
|
||||||
|
boundary!.right - draggedObject.width,
|
||||||
|
);
|
||||||
|
final double top = clampDouble(
|
||||||
|
draggedObject.top,
|
||||||
|
boundary!.top,
|
||||||
|
boundary!.bottom - draggedObject.height,
|
||||||
|
);
|
||||||
|
return Rect.fromLTWH(left, top, draggedObject.width, draggedObject.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides a [DragBoundaryDelegate] for its descendants whose bounds are those defined by this widget.
|
||||||
|
///
|
||||||
|
/// [forRectOf] and [forRectMaybeOf] returns a delegate for a drag object of type [Rect].
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example demonstrates dragging a red box, constrained within the bounds
|
||||||
|
/// of a green box.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/gesture_detector/gesture_detector.3.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
class DragBoundary extends InheritedWidget {
|
||||||
|
/// Creates a widget that provides a boundary to its descendants.
|
||||||
|
const DragBoundary({required super.child, super.key});
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.DragBoundary.forRectOf}
|
||||||
|
/// Retrieve the [DragBoundary] from the nearest ancestor to
|
||||||
|
/// get its [DragBoundaryDelegate] of [Rect].
|
||||||
|
///
|
||||||
|
/// The [useGlobalPosition] specifies whether to retrieve the [DragBoundaryDelegate] of type
|
||||||
|
/// [Rect] in global coordinates. If false, the local coordinates of the boundary are used. Defaults to true.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// If no [DragBoundary] ancestor is found, the delegate will return a delegate that allows the drag object to move freely.
|
||||||
|
static DragBoundaryDelegate<Rect> forRectOf(BuildContext context, {bool useGlobalPosition = true}) {
|
||||||
|
return forRectMaybeOf(context, useGlobalPosition: useGlobalPosition)
|
||||||
|
?? _DragBoundaryDelegateForRect(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.DragBoundary.forRectOf}
|
||||||
|
///
|
||||||
|
/// returns null if not ancestor is found.
|
||||||
|
static DragBoundaryDelegate<Rect>? forRectMaybeOf(BuildContext context, {bool useGlobalPosition = true}) {
|
||||||
|
final InheritedElement? element =
|
||||||
|
context.getElementForInheritedWidgetOfExactType<DragBoundary>();
|
||||||
|
if (element == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final RenderBox? rb = element.findRenderObject() as RenderBox?;
|
||||||
|
assert(rb != null && rb.hasSize, 'DragBoundary is not available');
|
||||||
|
final Rect boundary = useGlobalPosition
|
||||||
|
? Rect.fromPoints(rb!.localToGlobal(Offset.zero), rb.localToGlobal(rb.size.bottomRight(Offset.zero)))
|
||||||
|
: Offset.zero & rb!.size;
|
||||||
|
return _DragBoundaryDelegateForRect(boundary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,7 @@ export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
|
|||||||
export 'src/widgets/dismissible.dart';
|
export 'src/widgets/dismissible.dart';
|
||||||
export 'src/widgets/display_feature_sub_screen.dart';
|
export 'src/widgets/display_feature_sub_screen.dart';
|
||||||
export 'src/widgets/disposable_build_context.dart';
|
export 'src/widgets/disposable_build_context.dart';
|
||||||
|
export 'src/widgets/drag_boundary.dart';
|
||||||
export 'src/widgets/drag_target.dart';
|
export 'src/widgets/drag_target.dart';
|
||||||
export 'src/widgets/draggable_scrollable_sheet.dart';
|
export 'src/widgets/draggable_scrollable_sheet.dart';
|
||||||
export 'src/widgets/dual_transition_builder.dart';
|
export 'src/widgets/dual_transition_builder.dart';
|
||||||
|
77
packages/flutter/test/widgets/drag_boundary_test.dart
Normal file
77
packages/flutter/test/widgets/drag_boundary_test.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 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/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('test DragBoundary with useGlobalPosition', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 100, left: 100),
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: DragBoundary(
|
||||||
|
child: SizedBox(
|
||||||
|
key: key,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final DragBoundaryDelegate<Rect> boundary = DragBoundary.forRectOf(key.currentContext!);
|
||||||
|
expect(boundary, isNotNull);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(50, 50, 20, 20)), isFalse);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(100, 100, 20, 20)), isTrue);
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(50, 50, 20, 20)), const Rect.fromLTWH(100, 100, 20, 20));
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(150, 150, 20, 20)), const Rect.fromLTWH(150, 150, 20, 20));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('test DragBoundary without useGlobalPosition', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 100, left: 100),
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: DragBoundary(
|
||||||
|
child: SizedBox(
|
||||||
|
key: key,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final DragBoundaryDelegate<Rect> boundary = DragBoundary.forRectOf(key.currentContext!, useGlobalPosition: false);
|
||||||
|
expect(boundary, isNotNull);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(50, 50, 20, 20)), isTrue);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(90, 90, 20, 20)), isFalse);
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(50, 50, 20, 20)), const Rect.fromLTWH(50, 50, 20, 20));
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(90, 90, 20, 20)), const Rect.fromLTWH(80, 80, 20, 20));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('forRectOf should return a free boundary when no ancestor has a DragBoundary', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 100, left: 100),
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
key: key,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final DragBoundaryDelegate<Rect> boundary = DragBoundary.forRectOf(key.currentContext!);
|
||||||
|
expect(boundary, isNotNull);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(50, 50, 20, 20)), isTrue);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(100, 100, 20, 20)), isTrue);
|
||||||
|
expect(boundary.isWithinBoundary(const Rect.fromLTWH(300, 300, 300, 300)), isTrue);
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(50, 50, 20, 20)), const Rect.fromLTWH(50, 50, 20, 20));
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(150, 150, 20, 20)), const Rect.fromLTWH(150, 150, 20, 20));
|
||||||
|
expect(boundary.nearestPositionWithinBoundary(const Rect.fromLTWH(300, 300, 300, 300)), const Rect.fromLTWH(300, 300, 300, 300));
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user