Make RenderUiKitView reject absorbed touch events (#28666)
When a touch event that is in the bounds of a RenderUiKitView is absorbed by another render object,
the RenderUiKitView's handleEvent is not called for that object. On the platform side, the touch event hits the FlutterTouchInterceptingView which is waiting for a framework decision that never arrived on whether to reject or accept the gesture.
This change fixes the issue by having RenderUiKitView register a global PointerRoute, that is used to reject absorbed touch events.
diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart
index 189cedf..d816939 100644
--- a/packages/flutter/lib/src/rendering/platform_view.dart
+++ b/packages/flutter/lib/src/rendering/platform_view.dart
@@ -323,6 +323,8 @@
_UiKitViewGestureRecognizer _gestureRecognizer;
+ PointerEvent _lastPointerDownEvent;
+
@override
void performResize() {
size = constraints.biggest;
@@ -349,13 +351,41 @@
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
- if (event is PointerDownEvent) {
- _gestureRecognizer.addPointer(event);
+ if (event is! PointerDownEvent) {
+ return;
}
+ _gestureRecognizer.addPointer(event);
+ _lastPointerDownEvent = event;
+ }
+
+ // This is registered as a global PointerRoute while the render object is attached.
+ void _handleGlobalPointerEvent(PointerEvent event) {
+ if (event is! PointerDownEvent) {
+ return;
+ }
+ final Offset localOffset = globalToLocal(event.position);
+ if(!(Offset.zero & size).contains(localOffset)) {
+ return;
+ }
+ if (event != _lastPointerDownEvent) {
+ // The pointer event is in the bounds of this render box, but we didn't get it in handleEvent.
+ // This means that the pointer event was absorbed by a different render object.
+ // Since on the platform side the FlutterTouchIntercepting view is seeing all events that are
+ // within its bounds we need to tell it to reject the current touch sequence.
+ _viewController.rejectGesture();
+ }
+ _lastPointerDownEvent = null;
+ }
+
+ @override
+ void attach(PipelineOwner owner) {
+ super.attach(owner);
+ GestureBinding.instance.pointerRouter.addGlobalRoute(_handleGlobalPointerEvent);
}
@override
void detach() {
+ GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
_gestureRecognizer.reset();
super.detach();
}
diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart
index 3eb017a..4b4ecae 100644
--- a/packages/flutter/test/widgets/platform_view_test.dart
+++ b/packages/flutter/test/widgets/platform_view_test.dart
@@ -1364,6 +1364,36 @@
expect(viewsController.gesturesRejected[currentViewId + 1], 0);
});
+ testWidgets('UiKitView rejects gestures absorbed by siblings', (WidgetTester tester) async {
+ final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
+ final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
+ viewsController.registerViewType('webview');
+
+ await tester.pumpWidget(
+ Stack(
+ alignment: Alignment.topLeft,
+ children: <Widget>[
+ const UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
+ Container(
+ color: const Color.fromARGB(255, 255, 255, 255),
+ width: 100,
+ height: 100,
+ ),
+ ],
+ )
+ );
+
+ // First frame is before the platform view was created so the render object
+ // is not yet in the tree.
+ await tester.pump();
+
+ final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
+ await gesture.up();
+
+ expect(viewsController.gesturesRejected[currentViewId + 1], 1);
+ expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
+ });
+
testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async {
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');