Unverified Commit cff67336 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add viewId to PointerEvents (#128287)

Follow-up to https://github.com/flutter/engine/pull/42493.
parent 31e3ae89
......@@ -288,7 +288,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
try {
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, platformDispatcher.implicitView!.devicePixelRatio));
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, _devicePixelRatioForView));
if (!locked) {
_flushPointerEventQueue();
}
......@@ -302,6 +302,10 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
}
}
double? _devicePixelRatioForView(int viewId) {
return platformDispatcher.view(id: viewId)?.devicePixelRatio;
}
/// Dispatch a [PointerCancelEvent] for the given pointer soon.
///
/// The pointer event will be dispatched before the next pointer event and
......@@ -368,7 +372,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
assert(!_hitTests.containsKey(event.pointer), 'Pointer of ${event.toString(minLevel: DiagnosticLevel.debug)} unexpectedly has a HitTestResult associated with it.');
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
hitTestInView(hitTestResult, event.position, event.viewId);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
_hitTests[event.pointer] = hitTestResult;
}
......@@ -401,12 +405,22 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
}
}
/// Determine which [HitTestTarget] objects are located at a given position.
/// Determine which [HitTestTarget] objects are located at a given position in
/// the specified view.
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
void hitTestInView(HitTestResult result, Offset position, int viewId) {
result.add(HitTestEntry(this));
}
@override // from HitTestable
@Deprecated(
'Use hitTestInView and specify the view to hit test. '
'This feature was deprecated after v3.11.0-20.0.pre.',
)
void hitTest(HitTestResult result, Offset position) {
hitTestInView(result, position, platformDispatcher.implicitView!.viewId);
}
/// Dispatch an event to [pointerRouter] and the path of a hit test result.
///
/// The `event` is routed to [pointerRouter]. If the `hitTestResult` is not
......
......@@ -32,6 +32,18 @@ int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
}
}
/// Signature for a callback that returns the device pixel ratio of a
/// [FlutterView] identified by the provided `viewId`.
///
/// Returns null if no view with the provided ID exists.
///
/// Used by [PointerEventConverter.expand].
///
/// See also:
///
/// * [FlutterView.devicePixelRatio] for an explanation of device pixel ratio.
typedef DevicePixelRatioGetter = double? Function(int viewId);
/// Converts from engine pointer data to framework pointer events.
///
/// This takes [PointerDataPacket] objects, as received from the engine via
......@@ -45,10 +57,15 @@ abstract final class PointerEventConverter {
/// [dart:ui.FlutterView.devicePixelRatio]) is used to convert the incoming data
/// from physical coordinates to logical pixels. See the discussion at
/// [PointerEvent] for more details on the [PointerEvent] coordinate space.
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) {
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, DevicePixelRatioGetter devicePixelRatioForView) {
return data
.where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown)
.map<PointerEvent?>((ui.PointerData datum) {
final double? devicePixelRatio = devicePixelRatioForView(datum.viewId);
if (devicePixelRatio == null) {
// View doesn't exist anymore.
return null;
}
final Offset position = Offset(datum.physicalX, datum.physicalY) / devicePixelRatio;
final Offset delta = Offset(datum.physicalDeltaX, datum.physicalDeltaY) / devicePixelRatio;
final double radiusMinor = _toLogicalPixels(datum.radiusMinor, devicePixelRatio);
......@@ -62,6 +79,7 @@ abstract final class PointerEventConverter {
switch (datum.change) {
case ui.PointerChange.add:
return PointerAddedEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
kind: kind,
device: datum.device,
......@@ -79,6 +97,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.hover:
return PointerHoverEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
kind: kind,
device: datum.device,
......@@ -102,6 +121,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.down:
return PointerDownEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
......@@ -124,6 +144,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.move:
return PointerMoveEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
......@@ -149,6 +170,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.up:
return PointerUpEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
......@@ -172,6 +194,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.cancel:
return PointerCancelEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
......@@ -194,6 +217,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.remove:
return PointerRemovedEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
kind: kind,
device: datum.device,
......@@ -208,6 +232,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.panZoomStart:
return PointerPanZoomStartEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
device: datum.device,
......@@ -221,6 +246,7 @@ abstract final class PointerEventConverter {
final Offset panDelta =
Offset(datum.panDeltaX, datum.panDeltaY) / devicePixelRatio;
return PointerPanZoomUpdateEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
device: datum.device,
......@@ -234,6 +260,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerChange.panZoomEnd:
return PointerPanZoomEndEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
device: datum.device,
......@@ -249,6 +276,7 @@ abstract final class PointerEventConverter {
final Offset scrollDelta =
Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio;
return PointerScrollEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
kind: kind,
device: datum.device,
......@@ -258,6 +286,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerSignalKind.scrollInertiaCancel:
return PointerScrollInertiaCancelEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
kind: kind,
device: datum.device,
......@@ -266,6 +295,7 @@ abstract final class PointerEventConverter {
);
case ui.PointerSignalKind.scale:
return PointerScaleEvent(
viewId: datum.viewId,
timeStamp: timeStamp,
kind: kind,
device: datum.device,
......
......@@ -16,11 +16,16 @@ export 'events.dart' show PointerEvent;
/// An object that can hit-test pointers.
abstract interface class HitTestable {
/// Check whether the given position hits this object.
///
/// If this given position hits this object, consider adding a [HitTestEntry]
/// to the given hit test result.
/// Deprecated. Use [hitTestInView] instead.
@Deprecated(
'Use hitTestInView and specify the view to hit test. '
'This feature was deprecated after v3.11.0-20.0.pre.',
)
void hitTest(HitTestResult result, Offset position);
/// Fills the provided [HitTestResult] with [HitTestEntry]s for objects that
/// are hit at the given `position` in the view identified by `viewId`.
void hitTestInView(HitTestResult result, Offset position, int viewId);
}
/// An object that can dispatch events.
......
......@@ -54,6 +54,7 @@ class PointerEventResampler {
int buttons,
) {
return PointerHoverEvent(
viewId: event.viewId,
timeStamp: timeStamp,
kind: event.kind,
device: event.device,
......@@ -86,6 +87,7 @@ class PointerEventResampler {
int buttons,
) {
return PointerMoveEvent(
viewId: event.viewId,
timeStamp: timeStamp,
pointer: pointerIdentifier,
kind: event.kind,
......
......@@ -519,9 +519,10 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
}
@override
void hitTest(HitTestResult result, Offset position) {
void hitTestInView(HitTestResult result, Offset position, int viewId) {
assert(viewId == renderView.flutterView.viewId);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
super.hitTestInView(result, position, viewId);
}
Future<void> _forceRepaint() {
......
......@@ -103,6 +103,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
markNeedsLayout();
}
/// The [FlutterView] into which this [RenderView] will render.
ui.FlutterView get flutterView => _view;
final ui.FlutterView _view;
/// Whether Flutter should automatically compute the desired system UI.
......
......@@ -13,6 +13,7 @@ import 'debug.dart';
import 'framework.dart';
import 'media_query.dart';
import 'overlay.dart';
import 'view.dart';
/// Signature for determining whether the given data will be accepted by a [DragTarget].
///
......@@ -497,6 +498,7 @@ class _DraggableState<T extends Object> extends State<Draggable<T>> {
feedbackOffset: widget.feedbackOffset,
ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
ignoringFeedbackPointer: widget.ignoringFeedbackPointer,
viewId: View.of(context).viewId,
onDragUpdate: (DragUpdateDetails details) {
if (mounted && widget.onDragUpdate != null) {
widget.onDragUpdate!(details);
......@@ -756,6 +758,7 @@ class _DragAvatar<T extends Object> extends Drag {
this.onDragEnd,
required this.ignoringFeedbackSemantics,
required this.ignoringFeedbackPointer,
required this.viewId,
}) : _position = initialPosition {
_entry = OverlayEntry(builder: _build);
overlayState.insert(_entry!);
......@@ -772,6 +775,7 @@ class _DragAvatar<T extends Object> extends Drag {
final OverlayState overlayState;
final bool ignoringFeedbackSemantics;
final bool ignoringFeedbackPointer;
final int viewId;
_DragTargetState<Object>? _activeTarget;
final List<_DragTargetState<Object>> _enteredTargets = <_DragTargetState<Object>>[];
......@@ -804,7 +808,7 @@ class _DragAvatar<T extends Object> extends Drag {
_lastOffset = globalPosition - dragStartPoint;
_entry!.markNeedsBuild();
final HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset);
WidgetsBinding.instance.hitTestInView(result, globalPosition + feedbackOffset, viewId);
final List<_DragTargetState<Object>> targets = _getDragTargets(result.path).toList();
......
......@@ -5018,7 +5018,7 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> implements Scrib
}
final Rect intersection = calculatedBounds.intersect(rect);
final HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, intersection.center);
WidgetsBinding.instance.hitTestInView(result, intersection.center, View.of(context).viewId);
return result.path.any((HitTestEntry entry) => entry.target == renderEditable);
}
......
......@@ -39,42 +39,50 @@ void main() {
final ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.add,
timeStamp: epoch,
viewId: tester.view.viewId,
change: ui.PointerChange.add,
timeStamp: epoch,
),
ui.PointerData(
change: ui.PointerChange.down,
timeStamp: epoch,
viewId: tester.view.viewId,
change: ui.PointerChange.down,
timeStamp: epoch,
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 15.0,
timeStamp: epoch + const Duration(milliseconds: 10),
viewId: tester.view.viewId,
change: ui.PointerChange.move,
physicalX: 15.0,
timeStamp: epoch + const Duration(milliseconds: 10),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 30.0,
timeStamp: epoch + const Duration(milliseconds: 20),
viewId: tester.view.viewId,
change: ui.PointerChange.move,
physicalX: 30.0,
timeStamp: epoch + const Duration(milliseconds: 20),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 45.0,
timeStamp: epoch + const Duration(milliseconds: 30),
viewId: tester.view.viewId,
change: ui.PointerChange.move,
physicalX: 45.0,
timeStamp: epoch + const Duration(milliseconds: 30),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 50.0,
timeStamp: epoch + const Duration(milliseconds: 40),
viewId: tester.view.viewId,
change: ui.PointerChange.move,
physicalX: 50.0,
timeStamp: epoch + const Duration(milliseconds: 40),
),
ui.PointerData(
change: ui.PointerChange.up,
physicalX: 60.0,
timeStamp: epoch + const Duration(milliseconds: 40),
viewId: tester.view.viewId,
change: ui.PointerChange.up,
physicalX: 60.0,
timeStamp: epoch + const Duration(milliseconds: 40),
),
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 60.0,
timeStamp: epoch + const Duration(milliseconds: 40),
viewId: tester.view.viewId,
change: ui.PointerChange.remove,
physicalX: 60.0,
timeStamp: epoch + const Duration(milliseconds: 40),
),
],
);
......
......@@ -175,7 +175,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0], isA<PointerAddedEvent>());
......@@ -191,7 +191,7 @@ void main() {
ui.PointerData(change: ui.PointerChange.add, device: 24),
],
);
List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 1);
expect(events[0], isA<PointerAddedEvent>());
......@@ -207,7 +207,7 @@ void main() {
ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaY: double.negativeInfinity, scrollDeltaX: 10),
],
);
events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 0);
// Send packet with a valid scroll event.
......@@ -217,12 +217,12 @@ void main() {
],
);
// Make sure PointerEventConverter can expand when device pixel ratio is valid.
events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 1);
expect(events[0], isA<PointerScrollEvent>());
// Make sure PointerEventConverter returns none when device pixel ratio is invalid.
events = PointerEventConverter.expand(packet.data, 0).toList();
events = PointerEventConverter.expand(packet.data, (int viewId) => 0).toList();
expect(events.length, 0);
});
......@@ -234,7 +234,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 2);
expect(events[0], isA<PointerAddedEvent>());
......@@ -253,7 +253,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0], isA<PointerAddedEvent>());
......@@ -280,7 +280,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0], isA<PointerAddedEvent>());
......@@ -312,7 +312,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0], isA<PointerAddedEvent>());
......@@ -341,7 +341,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0], isA<PointerAddedEvent>());
......@@ -371,7 +371,7 @@ void main() {
],
);
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList();
final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
expect(events.length, 5);
expect(events[0], isA<PointerAddedEvent>());
......@@ -429,4 +429,33 @@ void main() {
FlutterError.onError = FlutterError.presentError;
}
});
test('PointerEventConverter processes view IDs', () {
const int startID = 987654;
const List<ui.PointerData> data = <ui.PointerData>[
ui.PointerData(viewId: startID + 0, change: ui.PointerChange.cancel), // ignore: avoid_redundant_argument_values
ui.PointerData(viewId: startID + 1, change: ui.PointerChange.add),
ui.PointerData(viewId: startID + 2, change: ui.PointerChange.remove),
ui.PointerData(viewId: startID + 3, change: ui.PointerChange.hover),
ui.PointerData(viewId: startID + 4, change: ui.PointerChange.down),
ui.PointerData(viewId: startID + 5, change: ui.PointerChange.move),
ui.PointerData(viewId: startID + 6, change: ui.PointerChange.up),
ui.PointerData(viewId: startID + 7, change: ui.PointerChange.panZoomStart),
ui.PointerData(viewId: startID + 8, change: ui.PointerChange.panZoomUpdate),
ui.PointerData(viewId: startID + 9, change: ui.PointerChange.panZoomEnd),
];
final List<int> viewIds = <int>[];
double devicePixelRatioGetter(int viewId) {
viewIds.add(viewId);
return viewId / 10.0;
}
final List<PointerEvent> events = PointerEventConverter.expand(data, devicePixelRatioGetter).toList();
final List<int> expectedViewIds = List<int>.generate(10, (int index) => startID + index);
expect(viewIds, expectedViewIds);
expect(events, hasLength(10));
expect(events.map((PointerEvent event) => event.viewId), expectedViewIds);
});
}
......@@ -1192,7 +1192,8 @@ abstract class WidgetController {
/// Forwards the given location to the binding's hitTest logic.
HitTestResult hitTestOnBinding(Offset location) {
final HitTestResult result = HitTestResult();
binding.hitTest(result, location);
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
binding.hitTest(result, location); // ignore: deprecated_member_use
return result;
}
......@@ -1313,7 +1314,8 @@ abstract class WidgetController {
final Offset location = box.localToGlobal(sizeToPoint(box.size));
if (warnIfMissed) {
final HitTestResult result = HitTestResult();
binding.hitTest(result, location);
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
binding.hitTest(result, location); // ignore: deprecated_member_use
bool found = false;
for (final HitTestEntry entry in result.path) {
if (entry.target == box) {
......
......@@ -642,7 +642,8 @@ class _HitTestableFinder extends ChainedFinder {
final RenderBox box = candidate.renderObject! as RenderBox;
final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
final HitTestResult hitResult = HitTestResult();
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset); // ignore: deprecated_member_use
for (final HitTestEntry entry in hitResult.path) {
if (entry.target == candidate.renderObject) {
yield candidate;
......
......@@ -167,8 +167,8 @@ class TestPlatformDispatcher implements PlatformDispatcher {
: null;
}
final Map<Object, TestFlutterView> _testViews = <Object, TestFlutterView>{};
final Map<Object, TestDisplay> _testDisplays = <Object, TestDisplay>{};
final Map<int, TestFlutterView> _testViews = <int, TestFlutterView>{};
final Map<int, TestDisplay> _testDisplays = <int, TestDisplay>{};
@override
VoidCallback? get onMetricsChanged => _platformDispatcher.onMetricsChanged;
......@@ -510,6 +510,9 @@ class TestPlatformDispatcher implements PlatformDispatcher {
@override
Iterable<TestFlutterView> get views => _testViews.values;
@override
FlutterView? view({required int id}) => _testViews[id];
@override
Iterable<TestDisplay> get displays => _testDisplays.values;
......
......@@ -153,6 +153,10 @@ void main() {
retrieveTestBinding(tester).platformDispatcher.localesTestValue = defaultLocales;
});
testWidgets('TestPlatformDispatcher.view getter returns the implicit view', (WidgetTester tester) async {
expect(WidgetsBinding.instance.platformDispatcher.view(id: tester.view.viewId), same(tester.view));
});
// TODO(pdblasi-google): Removed this group of tests when the Display API is stable and supported on all platforms.
group('TestPlatformDispatcher with unsupported Display API', () {
testWidgets('can initialize with empty displays', (WidgetTester tester) async {
......
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