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/display_feature_sub_screen.dart';
|
||||
export 'src/widgets/disposable_build_context.dart';
|
||||
export 'src/widgets/drag_boundary.dart';
|
||||
export 'src/widgets/drag_target.dart';
|
||||
export 'src/widgets/draggable_scrollable_sheet.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