From 4d5e40675ba2e79237f8f3ab0e56c53a82930a42 Mon Sep 17 00:00:00 2001 From: Hixie Date: Tue, 9 Feb 2016 15:46:16 -0800 Subject: [PATCH] Tapping through drag targets. Factor out the HitTestBehavior logic so that RenderMetaData can use it. Use that in DragTarget. --- .../flutter/lib/src/rendering/proxy_box.dart | 117 +++++++++++------- packages/flutter/lib/src/widgets/basic.dart | 21 +++- .../flutter/lib/src/widgets/drag_target.dart | 1 + .../flutter/test/widget/draggable_test.dart | 103 ++++++++++++++- 4 files changed, 189 insertions(+), 53 deletions(-) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index c65f2967954..7fe093b1e33 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -91,6 +91,61 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin= 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 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 PointerMoveEventListener(PointerMoveEvent event); 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 @@ class RenderPointerListener extends RenderProxyBox { if (listeners.isEmpty) listeners.add(''); 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 @@ class RenderIgnorePointer extends RenderProxyBox { } } -/// 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 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 09c581da141..a47685e4f45 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -2193,20 +2193,31 @@ class ExcludeSemantics extends OneChildRenderObjectWidget { } 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 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 ccff03b79ba..67944e4abf5 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -288,6 +288,7 @@ class _DragTargetState extends State> { Widget build(BuildContext context) { return new MetaData( metaData: this, + behavior: HitTestBehavior.translucent, child: config.builder(context, new UnmodifiableListView(_candidateData), new UnmodifiableListView(_rejectedData) diff --git a/packages/flutter/test/widget/draggable_test.dart b/packages/flutter/test/widget/draggable_test.dart index 30c6435ab6f..831461733b9 100644 --- a/packages/flutter/test/widget/draggable_test.dart +++ b/packages/flutter/test/widget/draggable_test.dart @@ -11,7 +11,7 @@ void main() { testWidgets((WidgetTester tester) { TestPointer pointer = new TestPointer(7); - List accepted = []; + List accepted = []; tester.pumpWidget(new MaterialApp( routes: { @@ -70,4 +70,105 @@ void main() { expect(tester.findText('Target'), isNotNull); }); }); + + test('Drag and drop - dragging over button', () { + testWidgets((WidgetTester tester) { + TestPointer pointer = new TestPointer(7); + + List events = []; + Point firstLocation, secondLocation; + + tester.pumpWidget(new MaterialApp( + routes: { + '/': (RouteArguments args) { return new Column( + children: [ + new Draggable( + data: 1, + child: new Text('Source'), + feedback: new Text('Dragging') + ), + new Stack( + children: [ + 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(['tap'])); + events.clear(); + + expect(events, isEmpty); + tester.tap(tester.findText('Target')); + expect(events, equals(['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(['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(['tap', 'tap', 'drop'])); + events.clear(); + + }); + }); }