diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 67f665ad85cab..c848d0486b196 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -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 diff --git a/packages/flutter/lib/src/gestures/converter.dart b/packages/flutter/lib/src/gestures/converter.dart index e40f29e02e91a..4892259eafdcb 100644 --- a/packages/flutter/lib/src/gestures/converter.dart +++ b/packages/flutter/lib/src/gestures/converter.dart @@ -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 expand(Iterable data, double devicePixelRatio) { + static Iterable expand(Iterable data, DevicePixelRatioGetter devicePixelRatioForView) { return data .where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown) .map((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, diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index 1a1b5f2b576cc..be147d6d154cc 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:ui' show Offset, PointerDeviceKind; import 'package:flutter/foundation.dart'; @@ -245,6 +244,7 @@ abstract class PointerEvent with Diagnosticable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const PointerEvent({ + this.viewId = 0, this.embedderId = 0, this.timeStamp = Duration.zero, this.pointer = 0, @@ -273,6 +273,9 @@ abstract class PointerEvent with Diagnosticable { this.original, }); + /// The ID of the [FlutterView] which this event originated from. + final int viewId; + /// Unique identifier that ties the [PointerEvent] to the embedder event that created it. /// /// No two pointer events can have the same [embedderId] on platforms that set it. @@ -536,6 +539,7 @@ abstract class PointerEvent with Diagnosticable { /// Calling this method on a transformed event will return a new transformed /// event based on the current [transform] and the provided properties. PointerEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -619,7 +623,6 @@ abstract class PointerEvent with Diagnosticable { // A mixin that adds implementation for [debugFillProperties] and [toStringFull] // to [PointerEvent]. mixin _PointerEventDescription on PointerEvent { - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -650,6 +653,7 @@ mixin _PointerEventDescription on PointerEvent { properties.add(FlagProperty('obscured', value: obscured, ifTrue: 'obscured', level: DiagnosticLevel.debug)); properties.add(FlagProperty('synthesized', value: synthesized, ifTrue: 'synthesized', level: DiagnosticLevel.debug)); properties.add(IntProperty('embedderId', embedderId, defaultValue: 0, level: DiagnosticLevel.debug)); + properties.add(IntProperty('viewId', viewId, defaultValue: 0, level: DiagnosticLevel.debug)); } /// Returns a complete textual description of this event. @@ -666,7 +670,6 @@ abstract class _AbstractPointerEvent implements PointerEvent { } // matrix. It defers all field getters to the original event, except for // [localPosition] and [localDelta], which are calculated when first used. abstract class _TransformedPointerEvent extends _AbstractPointerEvent with Diagnosticable, _PointerEventDescription { - @override PointerEvent get original; @@ -758,11 +761,15 @@ abstract class _TransformedPointerEvent extends _AbstractPointerEvent with Diagn untransformedEndPosition: position, transformedEndPosition: localPosition, ); + + @override + int get viewId => original.viewId; } mixin _CopyPointerAddedEvent on PointerEvent { @override PointerAddedEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -787,6 +794,7 @@ mixin _CopyPointerAddedEvent on PointerEvent { int? embedderId, }) { return PointerAddedEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -814,6 +822,7 @@ class PointerAddedEvent extends PointerEvent with _PointerEventDescription, _Cop /// /// All of the arguments must be non-null. const PointerAddedEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, @@ -858,6 +867,7 @@ class _TransformedPointerAddedEvent extends _TransformedPointerEvent with _CopyP mixin _CopyPointerRemovedEvent on PointerEvent { @override PointerRemovedEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -882,6 +892,7 @@ mixin _CopyPointerRemovedEvent on PointerEvent { int? embedderId, }) { return PointerRemovedEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -906,6 +917,7 @@ class PointerRemovedEvent extends PointerEvent with _PointerEventDescription, _C /// /// All of the arguments must be non-null. const PointerRemovedEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, @@ -948,6 +960,7 @@ class _TransformedPointerRemovedEvent extends _TransformedPointerEvent with _Cop mixin _CopyPointerHoverEvent on PointerEvent { @override PointerHoverEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -972,6 +985,7 @@ mixin _CopyPointerHoverEvent on PointerEvent { int? embedderId, }) { return PointerHoverEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -1013,6 +1027,7 @@ class PointerHoverEvent extends PointerEvent with _PointerEventDescription, _Cop /// /// All of the arguments must be non-null. const PointerHoverEvent({ + super.viewId, super.timeStamp, super.kind, super.pointer, @@ -1064,6 +1079,7 @@ class _TransformedPointerHoverEvent extends _TransformedPointerEvent with _CopyP mixin _CopyPointerEnterEvent on PointerEvent { @override PointerEnterEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1088,6 +1104,7 @@ mixin _CopyPointerEnterEvent on PointerEvent { int? embedderId, }) { return PointerEnterEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -1129,6 +1146,7 @@ class PointerEnterEvent extends PointerEvent with _PointerEventDescription, _Cop /// /// All of the arguments must be non-null. const PointerEnterEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, @@ -1162,6 +1180,7 @@ class PointerEnterEvent extends PointerEvent with _PointerEventDescription, _Cop /// /// This is used by the [MouseTracker] to synthesize enter events. factory PointerEnterEvent.fromMouseEvent(PointerEvent event) => PointerEnterEvent( + viewId: event.viewId, timeStamp: event.timeStamp, pointer: event.pointer, kind: event.kind, @@ -1210,6 +1229,7 @@ class _TransformedPointerEnterEvent extends _TransformedPointerEvent with _CopyP mixin _CopyPointerExitEvent on PointerEvent { @override PointerExitEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1234,6 +1254,7 @@ mixin _CopyPointerExitEvent on PointerEvent { int? embedderId, }) { return PointerExitEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -1275,6 +1296,7 @@ class PointerExitEvent extends PointerEvent with _PointerEventDescription, _Copy /// /// All of the arguments must be non-null. const PointerExitEvent({ + super.viewId, super.timeStamp, super.kind, super.pointer, @@ -1306,6 +1328,7 @@ class PointerExitEvent extends PointerEvent with _PointerEventDescription, _Copy /// /// This is used by the [MouseTracker] to synthesize exit events. factory PointerExitEvent.fromMouseEvent(PointerEvent event) => PointerExitEvent( + viewId: event.viewId, timeStamp: event.timeStamp, pointer: event.pointer, kind: event.kind, @@ -1355,6 +1378,7 @@ class _TransformedPointerExitEvent extends _TransformedPointerEvent with _CopyPo mixin _CopyPointerDownEvent on PointerEvent { @override PointerDownEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1379,6 +1403,7 @@ mixin _CopyPointerDownEvent on PointerEvent { int? embedderId, }) { return PointerDownEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, pointer: pointer ?? this.pointer, kind: kind ?? this.kind, @@ -1413,6 +1438,7 @@ class PointerDownEvent extends PointerEvent with _PointerEventDescription, _Copy /// /// All of the arguments must be non-null. const PointerDownEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, @@ -1463,6 +1489,7 @@ class _TransformedPointerDownEvent extends _TransformedPointerEvent with _CopyPo mixin _CopyPointerMoveEvent on PointerEvent { @override PointerMoveEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1487,6 +1514,7 @@ mixin _CopyPointerMoveEvent on PointerEvent { int? embedderId, }) { return PointerMoveEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, pointer: pointer ?? this.pointer, kind: kind ?? this.kind, @@ -1526,6 +1554,7 @@ class PointerMoveEvent extends PointerEvent with _PointerEventDescription, _Copy /// /// All of the arguments must be non-null. const PointerMoveEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, @@ -1580,6 +1609,7 @@ class _TransformedPointerMoveEvent extends _TransformedPointerEvent with _CopyPo mixin _CopyPointerUpEvent on PointerEvent { @override PointerUpEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1605,6 +1635,7 @@ mixin _CopyPointerUpEvent on PointerEvent { int? embedderId, }) { return PointerUpEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, pointer: pointer ?? this.pointer, kind: kind ?? this.kind, @@ -1640,6 +1671,7 @@ class PointerUpEvent extends PointerEvent with _PointerEventDescription, _CopyPo /// /// All of the arguments must be non-null. const PointerUpEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, @@ -1705,6 +1737,7 @@ abstract class PointerSignalEvent extends PointerEvent { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const PointerSignalEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind = PointerDeviceKind.mouse, @@ -1720,6 +1753,7 @@ mixin _CopyPointerScrollEvent on PointerEvent { @override PointerScrollEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1744,6 +1778,7 @@ mixin _CopyPointerScrollEvent on PointerEvent { int? embedderId, }) { return PointerScrollEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -1770,6 +1805,7 @@ class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescriptio /// /// All of the arguments must be non-null. const PointerScrollEvent({ + super.viewId, super.timeStamp, super.kind, super.device, @@ -1821,6 +1857,7 @@ class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _Copy mixin _CopyPointerScrollInertiaCancelEvent on PointerEvent { @override PointerScrollInertiaCancelEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1845,6 +1882,7 @@ mixin _CopyPointerScrollInertiaCancelEvent on PointerEvent { int? embedderId, }) { return PointerScrollInertiaCancelEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -1870,6 +1908,7 @@ class PointerScrollInertiaCancelEvent extends PointerSignalEvent with _PointerEv /// /// All of the arguments must be non-null. const PointerScrollInertiaCancelEvent({ + super.viewId, super.timeStamp, super.kind, super.device, @@ -1905,6 +1944,7 @@ mixin _CopyPointerScaleEvent on PointerEvent { @override PointerScaleEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -1930,6 +1970,7 @@ mixin _CopyPointerScaleEvent on PointerEvent { double? scale, }) { return PointerScaleEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, kind: kind ?? this.kind, device: device ?? this.device, @@ -1956,6 +1997,7 @@ class PointerScaleEvent extends PointerSignalEvent with _PointerEventDescription /// /// All of the arguments must be non-null. const PointerScaleEvent({ + super.viewId, super.timeStamp, super.kind, super.device, @@ -1995,6 +2037,7 @@ class _TransformedPointerScaleEvent extends _TransformedPointerEvent with _CopyP mixin _CopyPointerPanZoomStartEvent on PointerEvent { @override PointerPanZoomStartEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -2020,6 +2063,7 @@ mixin _CopyPointerPanZoomStartEvent on PointerEvent { }) { assert(kind == null || identical(kind, PointerDeviceKind.trackpad)); return PointerPanZoomStartEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, device: device ?? this.device, position: position ?? this.position, @@ -2039,6 +2083,7 @@ class PointerPanZoomStartEvent extends PointerEvent with _PointerEventDescriptio /// /// All of the arguments must be non-null. const PointerPanZoomStartEvent({ + super.viewId, super.timeStamp, super.device, super.pointer, @@ -2085,6 +2130,7 @@ mixin _CopyPointerPanZoomUpdateEvent on PointerEvent { @override PointerPanZoomUpdateEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -2116,6 +2162,7 @@ mixin _CopyPointerPanZoomUpdateEvent on PointerEvent { }) { assert(kind == null || identical(kind, PointerDeviceKind.trackpad)); return PointerPanZoomUpdateEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, device: device ?? this.device, position: position ?? this.position, @@ -2139,6 +2186,7 @@ class PointerPanZoomUpdateEvent extends PointerEvent with _PointerEventDescripti /// /// All of the arguments must be non-null. const PointerPanZoomUpdateEvent({ + super.viewId, super.timeStamp, super.device, super.pointer, @@ -2212,6 +2260,7 @@ class _TransformedPointerPanZoomUpdateEvent extends _TransformedPointerEvent wit mixin _CopyPointerPanZoomEndEvent on PointerEvent { @override PointerPanZoomEndEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -2237,6 +2286,7 @@ mixin _CopyPointerPanZoomEndEvent on PointerEvent { }) { assert(kind == null || identical(kind, PointerDeviceKind.trackpad)); return PointerPanZoomEndEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, device: device ?? this.device, position: position ?? this.position, @@ -2256,6 +2306,7 @@ class PointerPanZoomEndEvent extends PointerEvent with _PointerEventDescription, /// /// All of the arguments must be non-null. const PointerPanZoomEndEvent({ + super.viewId, super.timeStamp, super.device, super.pointer, @@ -2289,6 +2340,7 @@ class _TransformedPointerPanZoomEndEvent extends _TransformedPointerEvent with _ mixin _CopyPointerCancelEvent on PointerEvent { @override PointerCancelEvent copyWith({ + int? viewId, Duration? timeStamp, int? pointer, PointerDeviceKind? kind, @@ -2313,6 +2365,7 @@ mixin _CopyPointerCancelEvent on PointerEvent { int? embedderId, }) { return PointerCancelEvent( + viewId: viewId ?? this.viewId, timeStamp: timeStamp ?? this.timeStamp, pointer: pointer ?? this.pointer, kind: kind ?? this.kind, @@ -2347,6 +2400,7 @@ class PointerCancelEvent extends PointerEvent with _PointerEventDescription, _Co /// /// All of the arguments must be non-null. const PointerCancelEvent({ + super.viewId, super.timeStamp, super.pointer, super.kind, diff --git a/packages/flutter/lib/src/gestures/hit_test.dart b/packages/flutter/lib/src/gestures/hit_test.dart index 4a8fd11bfba51..b620872044ce0 100644 --- a/packages/flutter/lib/src/gestures/hit_test.dart +++ b/packages/flutter/lib/src/gestures/hit_test.dart @@ -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. diff --git a/packages/flutter/lib/src/gestures/resampler.dart b/packages/flutter/lib/src/gestures/resampler.dart index e2a67e0739a4f..0890097b81d2c 100644 --- a/packages/flutter/lib/src/gestures/resampler.dart +++ b/packages/flutter/lib/src/gestures/resampler.dart @@ -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, diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index fa40444a894f8..5a1f8969b09b3 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -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 _forceRepaint() { diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index a4aa7154e3890..a8cb7a2d3c0ca 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -103,6 +103,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin 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. diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index cf5fd21675ef4..f55806353bd5f 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -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 extends State> { 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 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 extends Drag { final OverlayState overlayState; final bool ignoringFeedbackSemantics; final bool ignoringFeedbackPointer; + final int viewId; _DragTargetState? _activeTarget; final List<_DragTargetState> _enteredTargets = <_DragTargetState>[]; @@ -804,7 +808,7 @@ class _DragAvatar 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> targets = _getDragTargets(result.path).toList(); diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 10e6220e4fb65..7187bfb58a9f7 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -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); } diff --git a/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart b/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart index 45050ce4c2125..26c2f8ffe3bd0 100644 --- a/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart +++ b/packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart @@ -39,42 +39,50 @@ void main() { final ui.PointerDataPacket packet = ui.PointerDataPacket( data: [ 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), ), ], ); diff --git a/packages/flutter/test/gestures/gesture_binding_test.dart b/packages/flutter/test/gestures/gesture_binding_test.dart index d4df7b403f74b..6663726713e6e 100644 --- a/packages/flutter/test/gestures/gesture_binding_test.dart +++ b/packages/flutter/test/gestures/gesture_binding_test.dart @@ -175,7 +175,7 @@ void main() { ], ); - final List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); expect(events[0], isA()); @@ -191,7 +191,7 @@ void main() { ui.PointerData(change: ui.PointerChange.add, device: 24), ], ); - List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 1); expect(events[0], isA()); @@ -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()); // 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 events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 2); expect(events[0], isA()); @@ -253,7 +253,7 @@ void main() { ], ); - final List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); expect(events[0], isA()); @@ -280,7 +280,7 @@ void main() { ], ); - final List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); expect(events[0], isA()); @@ -312,7 +312,7 @@ void main() { ], ); - final List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); expect(events[0], isA()); @@ -341,7 +341,7 @@ void main() { ], ); - final List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); expect(events[0], isA()); @@ -371,7 +371,7 @@ void main() { ], ); - final List events = PointerEventConverter.expand(packet.data, devicePixelRatio).toList(); + final List events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); expect(events[0], isA()); @@ -429,4 +429,33 @@ void main() { FlutterError.onError = FlutterError.presentError; } }); + + test('PointerEventConverter processes view IDs', () { + const int startID = 987654; + const List data = [ + 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 viewIds = []; + double devicePixelRatioGetter(int viewId) { + viewIds.add(viewId); + return viewId / 10.0; + } + + final List events = PointerEventConverter.expand(data, devicePixelRatioGetter).toList(); + + final List expectedViewIds = List.generate(10, (int index) => startID + index); + expect(viewIds, expectedViewIds); + expect(events, hasLength(10)); + expect(events.map((PointerEvent event) => event.viewId), expectedViewIds); + }); } diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 1d334300624c3..bdc1ab9ac7c0d 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -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) { diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 6a706318524d4..1e881489cb504 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -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; diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index cdd907390b18d..9c379003e7d9f 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -167,8 +167,8 @@ class TestPlatformDispatcher implements PlatformDispatcher { : null; } - final Map _testViews = {}; - final Map _testDisplays = {}; + final Map _testViews = {}; + final Map _testDisplays = {}; @override VoidCallback? get onMetricsChanged => _platformDispatcher.onMetricsChanged; @@ -510,6 +510,9 @@ class TestPlatformDispatcher implements PlatformDispatcher { @override Iterable get views => _testViews.values; + @override + FlutterView? view({required int id}) => _testViews[id]; + @override Iterable get displays => _testDisplays.values; diff --git a/packages/flutter_test/test/platform_dispatcher_test.dart b/packages/flutter_test/test/platform_dispatcher_test.dart index ae0b1d8cd6cd8..82544256e4dc0 100644 --- a/packages/flutter_test/test/platform_dispatcher_test.dart +++ b/packages/flutter_test/test/platform_dispatcher_test.dart @@ -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 {