Commit 4d5e4067 authored by Hixie's avatar Hixie

Tapping through drag targets.

Factor out the HitTestBehavior logic so that RenderMetaData can use it.

Use that in DragTarget.
parent a9a445b1
......@@ -91,6 +91,61 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox
}
}
/// 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 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('<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 @@ 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<String> settings) {
super.debugDescribeSettings(settings);
settings.add('metaData: $metaData');
}
}
/// Listens for the specified gestures from the semantics server (e.g.
......
......@@ -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<String> description) {
super.debugFillDescription(description);
description.add('$metaData');
description.add('behavior: $behavior');
description.add('metaData: $metaData');
}
}
......
......@@ -288,6 +288,7 @@ class _DragTargetState<T> extends State<DragTarget<T>> {
Widget build(BuildContext context) {
return new MetaData(
metaData: this,
behavior: HitTestBehavior.translucent,
child: config.builder(context,
new UnmodifiableListView<T>(_candidateData),
new UnmodifiableListView<dynamic>(_rejectedData)
......
......@@ -11,7 +11,7 @@ void main() {
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 @@ void main() {
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();
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment