Merge pull request #1744 from Hixie/tap-drag-target
Tapping through drag targets.
diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart
index c65f296..7fe093b 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -91,6 +91,61 @@
}
}
+/// How to behave during hit tests.
+enum HitTestBehavior {
+ /// Targets that defer to their children receive events within their bounds
+ /// only if one of their children is hit by the hit test.
+ deferToChild,
+
+ /// Opaque targets can be hit by hit tests, causing them to both receive
+ /// events within their bounds and prevent targets visually behind them from
+ /// also receiving events.
+ opaque,
+
+ /// Translucent targets both receive events within their bounds and permit
+ /// targets visually behind them to also receive events.
+ translucent,
+}
+
+/// A RenderProxyBox subclass that allows you to customize the
+/// hit-testing behavior.
+abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
+ RenderProxyBoxWithHitTestBehavior({
+ this.behavior: HitTestBehavior.deferToChild,
+ RenderBox child
+ }) : super(child);
+
+ HitTestBehavior behavior;
+
+ bool hitTest(HitTestResult result, { Point position }) {
+ bool hitTarget = false;
+ if (position.x >= 0.0 && position.x < size.width &&
+ position.y >= 0.0 && position.y < size.height) {
+ hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
+ if (hitTarget || behavior == HitTestBehavior.translucent)
+ result.add(new BoxHitTestEntry(this, position));
+ }
+ return hitTarget;
+ }
+
+ bool hitTestSelf(Point position) => behavior == HitTestBehavior.opaque;
+
+ void debugDescribeSettings(List<String> settings) {
+ super.debugDescribeSettings(settings);
+ switch (behavior) {
+ case HitTestBehavior.translucent:
+ settings.add('behavior: translucent');
+ break;
+ case HitTestBehavior.opaque:
+ settings.add('behavior: opaque');
+ break;
+ case HitTestBehavior.deferToChild:
+ settings.add('behavior: defer-to-child');
+ break;
+ }
+ }
+}
+
/// Imposes additional constraints on its child.
///
/// A render constrained box proxies most functions in the render box protocol
@@ -1231,51 +1286,21 @@
typedef void PointerUpEventListener(PointerUpEvent event);
typedef void PointerCancelEventListener(PointerCancelEvent event);
-/// How to behave during hit tests.
-enum HitTestBehavior {
- /// Targets that defer to their children receive events within their bounds
- /// only if one of their children is hit by the hit test.
- deferToChild,
-
- /// Opaque targets can be hit by hit tests, causing them to both receive
- /// events within their bounds and prevent targets visually behind them from
- /// also receiving events.
- opaque,
-
- /// Translucent targets both receive events within their bounds and permit
- /// targets visually behind them to also receive events.
- translucent,
-}
-
/// Invokes the callbacks in response to pointer events.
-class RenderPointerListener extends RenderProxyBox {
+class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
RenderPointerListener({
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerCancel,
- this.behavior: HitTestBehavior.deferToChild,
+ HitTestBehavior behavior: HitTestBehavior.deferToChild,
RenderBox child
- }) : super(child);
+ }) : super(behavior: behavior, child: child);
PointerDownEventListener onPointerDown;
PointerMoveEventListener onPointerMove;
PointerUpEventListener onPointerUp;
PointerCancelEventListener onPointerCancel;
- HitTestBehavior behavior;
-
- bool hitTest(HitTestResult result, { Point position }) {
- bool hitTarget = false;
- if (position.x >= 0.0 && position.x < size.width &&
- position.y >= 0.0 && position.y < size.height) {
- hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
- if (hitTarget || behavior == HitTestBehavior.translucent)
- result.add(new BoxHitTestEntry(this, position));
- }
- return hitTarget;
- }
-
- bool hitTestSelf(Point position) => behavior == HitTestBehavior.opaque;
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (onPointerDown != null && event is PointerDownEvent)
@@ -1302,17 +1327,6 @@
if (listeners.isEmpty)
listeners.add('<none>');
settings.add('listeners: ${listeners.join(", ")}');
- switch (behavior) {
- case HitTestBehavior.translucent:
- settings.add('behavior: translucent');
- break;
- case HitTestBehavior.opaque:
- settings.add('behavior: opaque');
- break;
- case HitTestBehavior.deferToChild:
- settings.add('behavior: defer-to-child');
- break;
- }
}
}
@@ -1392,12 +1406,21 @@
}
}
-/// Holds opaque meta data in the render tree
-class RenderMetaData extends RenderProxyBox {
- RenderMetaData({ RenderBox child, this.metaData }) : super(child);
+/// Holds opaque meta data in the render tree.
+class RenderMetaData extends RenderProxyBoxWithHitTestBehavior {
+ RenderMetaData({
+ this.metaData,
+ HitTestBehavior behavior: HitTestBehavior.deferToChild,
+ RenderBox child
+ }) : super(behavior: behavior, child: child);
/// Opaque meta data ignored by the render tree
dynamic metaData;
+
+ void debugDescribeSettings(List<String> settings) {
+ super.debugDescribeSettings(settings);
+ settings.add('metaData: $metaData');
+ }
}
/// Listens for the specified gestures from the semantics server (e.g.
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 09c581d..a47685e 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -2193,20 +2193,31 @@
}
class MetaData extends OneChildRenderObjectWidget {
- MetaData({ Key key, Widget child, this.metaData })
- : super(key: key, child: child);
+ MetaData({
+ Key key,
+ Widget child,
+ this.metaData,
+ this.behavior: HitTestBehavior.deferToChild
+ }) : super(key: key, child: child);
final dynamic metaData;
+ final HitTestBehavior behavior;
- RenderMetaData createRenderObject() => new RenderMetaData(metaData: metaData);
+ RenderMetaData createRenderObject() => new RenderMetaData(
+ metaData: metaData,
+ behavior: behavior
+ );
void updateRenderObject(RenderMetaData renderObject, MetaData oldWidget) {
- renderObject.metaData = metaData;
+ renderObject
+ ..metaData = metaData
+ ..behavior = behavior;
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
- description.add('$metaData');
+ description.add('behavior: $behavior');
+ description.add('metaData: $metaData');
}
}
diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart
index ccff03b..67944e4 100644
--- a/packages/flutter/lib/src/widgets/drag_target.dart
+++ b/packages/flutter/lib/src/widgets/drag_target.dart
@@ -288,6 +288,7 @@
Widget build(BuildContext context) {
return new MetaData(
metaData: this,
+ behavior: HitTestBehavior.translucent,
child: config.builder(context,
new UnmodifiableListView<T>(_candidateData),
new UnmodifiableListView<dynamic>(_rejectedData)
diff --git a/packages/flutter/test/widget/draggable_test.dart b/packages/flutter/test/widget/draggable_test.dart
index 30c6435..8314617 100644
--- a/packages/flutter/test/widget/draggable_test.dart
+++ b/packages/flutter/test/widget/draggable_test.dart
@@ -11,7 +11,7 @@
testWidgets((WidgetTester tester) {
TestPointer pointer = new TestPointer(7);
- List accepted = [];
+ List<dynamic> accepted = <dynamic>[];
tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{
@@ -70,4 +70,105 @@
expect(tester.findText('Target'), isNotNull);
});
});
+
+ test('Drag and drop - dragging over button', () {
+ testWidgets((WidgetTester tester) {
+ TestPointer pointer = new TestPointer(7);
+
+ List<String> events = <String>[];
+ Point firstLocation, secondLocation;
+
+ tester.pumpWidget(new MaterialApp(
+ routes: <String, RouteBuilder>{
+ '/': (RouteArguments args) { return new Column(
+ children: <Widget>[
+ new Draggable(
+ data: 1,
+ child: new Text('Source'),
+ feedback: new Text('Dragging')
+ ),
+ new Stack(
+ children: <Widget>[
+ new GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ events.add('tap');
+ },
+ child: new Container(
+ child: new Text('Button')
+ )
+ ),
+ new DragTarget(
+ builder: (context, data, rejects) {
+ return new IgnorePointer(
+ child: new Container(
+ child: new Text('Target')
+ )
+ );
+ },
+ onAccept: (data) {
+ events.add('drop');
+ }
+ ),
+ ]
+ ),
+ ]);
+ },
+ }
+ ));
+
+ expect(events, isEmpty);
+ expect(tester.findText('Source'), isNotNull);
+ expect(tester.findText('Dragging'), isNull);
+ expect(tester.findText('Target'), isNotNull);
+ expect(tester.findText('Button'), isNotNull);
+
+ // taps (we check both to make sure the test is consistent)
+
+ expect(events, isEmpty);
+ tester.tap(tester.findText('Button'));
+ expect(events, equals(<String>['tap']));
+ events.clear();
+
+ expect(events, isEmpty);
+ tester.tap(tester.findText('Target'));
+ expect(events, equals(<String>['tap']));
+ events.clear();
+
+ // drag and drop
+
+ firstLocation = tester.getCenter(tester.findText('Source'));
+ tester.dispatchEvent(pointer.down(firstLocation), firstLocation);
+ tester.pump();
+
+ secondLocation = tester.getCenter(tester.findText('Target'));
+ tester.dispatchEvent(pointer.move(secondLocation), firstLocation);
+ tester.pump();
+
+ expect(events, isEmpty);
+ tester.dispatchEvent(pointer.up(), firstLocation);
+ tester.pump();
+ expect(events, equals(<String>['drop']));
+ events.clear();
+
+ // drag and tap and drop
+
+ firstLocation = tester.getCenter(tester.findText('Source'));
+ tester.dispatchEvent(pointer.down(firstLocation), firstLocation);
+ tester.pump();
+
+ secondLocation = tester.getCenter(tester.findText('Target'));
+ tester.dispatchEvent(pointer.move(secondLocation), firstLocation);
+ tester.pump();
+
+ expect(events, isEmpty);
+ tester.tap(tester.findText('Button'));
+ tester.tap(tester.findText('Target'));
+ tester.dispatchEvent(pointer.up(), firstLocation);
+ tester.pump();
+ expect(events, equals(<String>['tap', 'tap', 'drop']));
+ events.clear();
+
+ });
+ });
}