mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge pull request #1744 from Hixie/tap-drag-target
Tapping through drag targets.
This commit is contained in:
commit
b2c710dd8d
@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user