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();
+
+    });
+  });
 }