diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 65b8e6cbea..20795b6d07 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -75,12 +75,12 @@ Set _factoriesTypeSet(Set> factories) { /// /// * [AndroidView] which is a widget that is used to show an Android view. /// * [PlatformViewsService] which is a service for controlling platform views. -class RenderAndroidView extends RenderBox { +class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { /// Creates a render object for an Android view. RenderAndroidView({ @required AndroidViewController viewController, - @required this.hitTestBehavior, + @required PlatformViewHitTestBehavior hitTestBehavior, @required Set> gestureRecognizers, }) : assert(viewController != null), assert(hitTestBehavior != null), @@ -89,6 +89,7 @@ class RenderAndroidView extends RenderBox { _motionEventsDispatcher = _MotionEventsDispatcher(globalToLocal, viewController); updateGestureRecognizers(gestureRecognizers); _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); + this.hitTestBehavior = hitTestBehavior; } _PlatformViewState _state = _PlatformViewState.uninitialized; @@ -117,11 +118,6 @@ class RenderAndroidView extends RenderBox { markNeedsSemanticsUpdate(); } - /// How to behave during hit testing. - // The implicit setter is enough here as changing this value will just affect - // any newly arriving events there's nothing we need to invalidate. - PlatformViewHitTestBehavior hitTestBehavior; - /// {@template flutter.rendering.platformView.updateGestureRecognizers} /// Updates which gestures should be forwarded to the platform view. /// @@ -139,16 +135,7 @@ class RenderAndroidView extends RenderBox { /// Any active gesture arena the Android view participates in is rejected when the /// set of gesture recognizers is changed. void updateGestureRecognizers(Set> gestureRecognizers) { - assert(gestureRecognizers != null); - assert( - _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, - 'There were multiple gesture recognizer factories for the same type, there must only be a single ' - 'gesture recognizer factory for each gesture recognizer type.',); - if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { - return; - } - _gestureRecognizer?.dispose(); - _gestureRecognizer = _AndroidViewGestureRecognizer(_motionEventsDispatcher, gestureRecognizers); + _updateGestureRecognizersWithCallBack(gestureRecognizers, _motionEventsDispatcher.handlePointerEvent); } @override @@ -162,8 +149,6 @@ class RenderAndroidView extends RenderBox { _MotionEventsDispatcher _motionEventsDispatcher; - _AndroidViewGestureRecognizer _gestureRecognizer; - @override void performResize() { size = constraints.biggest; @@ -229,24 +214,6 @@ class RenderAndroidView extends RenderBox { )); } - @override - bool hitTest(BoxHitTestResult result, { Offset position }) { - if (hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position)) - return false; - result.add(BoxHitTestEntry(this, position)); - return hitTestBehavior == PlatformViewHitTestBehavior.opaque; - } - - @override - bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent; - - @override - void handleEvent(PointerEvent event, HitTestEntry entry) { - if (event is PointerDownEvent) { - _gestureRecognizer.addPointer(event); - } - } - @override void describeSemanticsConfiguration (SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); @@ -257,12 +224,6 @@ class RenderAndroidView extends RenderBox { config.platformViewId = _viewController.id; } } - - @override - void detach() { - _gestureRecognizer.reset(); - super.detach(); - } } /// A render object for an iOS UIKit UIView. @@ -486,15 +447,17 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer { } } +typedef _HandlePointerEvent = void Function(PointerEvent event); + // This recognizer constructs gesture recognizers from a set of gesture recognizer factories -// it was give, adds all of them to a gesture arena team with the _AndroidViewGestureRecognizer +// it was give, adds all of them to a gesture arena team with the _PlatformViewGestureRecognizer // as the team captain. -// As long as ta gesture arena is unresolved the recognizer caches all pointer events. -// When the team wins the recognizer sends all the cached point events to the embedded Android view, and -// sets itself to a "forwarding mode" where it will forward any new pointer event to the Android view. -class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { - _AndroidViewGestureRecognizer( - this.dispatcher, +// As long as the gesture arena is unresolved, the recognizer caches all pointer events. +// When the team wins, the recognizer sends all the cached pointer events to `_handlePointerEvent`, and +// sets itself to a "forwarding mode" where it will forward any new pointer event to `_handlePointerEvent`. +class _PlatformViewGestureRecognizer extends OneSequenceGestureRecognizer { + _PlatformViewGestureRecognizer( + _HandlePointerEvent handlePointerEvent, this.gestureRecognizerFactories, { PointerDeviceKind kind, }) : super(kind: kind) { @@ -505,18 +468,19 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { return recognizerFactory.constructor()..team = team; }, ).toSet(); + _handlePointerEvent = handlePointerEvent; } - final _MotionEventsDispatcher dispatcher; + _HandlePointerEvent _handlePointerEvent; // Maps a pointer to a list of its cached pointer events. // Before the arena for a pointer is resolved all events are cached here, if we win the arena - // the cached events are dispatched to the view, if we lose the arena we clear the cache for + // the cached events are dispatched to `_handlePointerEvent`, if we lose the arena we clear the cache for // the pointer. final Map> cachedEvents = >{}; // Pointer for which we have already won the arena, events for pointers in this set are - // immediately dispatched to the Android view. + // immediately dispatched to `_handlePointerEvent`. final Set forwardedPointers = {}; // We use OneSequenceGestureRecognizers as they support gesture arena teams. @@ -534,7 +498,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { } @override - String get debugDescription => 'Android view'; + String get debugDescription => 'Platform view'; @override void didStopTrackingLastPointer(int pointer) { } @@ -542,16 +506,16 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { @override void handleEvent(PointerEvent event) { if (!forwardedPointers.contains(event.pointer)) { - cacheEvent(event); + _cacheEvent(event); } else { - dispatcher.handlePointerEvent(event); + _handlePointerEvent(event); } stopTrackingIfPointerNoLongerDown(event); } @override void acceptGesture(int pointer) { - flushPointerCache(pointer); + _flushPointerCache(pointer); forwardedPointers.add(pointer); } @@ -561,15 +525,15 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { cachedEvents.remove(pointer); } - void cacheEvent(PointerEvent event) { + void _cacheEvent(PointerEvent event) { if (!cachedEvents.containsKey(event.pointer)) { cachedEvents[event.pointer] = []; } cachedEvents[event.pointer].add(event); } - void flushPointerCache(int pointer) { - cachedEvents.remove(pointer)?.forEach(dispatcher.handlePointerEvent); + void _flushPointerCache(int pointer) { + cachedEvents.remove(pointer)?.forEach(_handlePointerEvent); } @override @@ -728,18 +692,24 @@ class _MotionEventsDispatcher { /// A render object for embedding a platform view. /// -/// [PlatformViewRenderBox] presents a platform view by adding a [PlatformViewLayer] layer, integrates it with the gesture arenas system -/// and adds relevant semantic nodes to the semantics tree. -class PlatformViewRenderBox extends RenderBox { +/// [PlatformViewRenderBox] presents a platform view by adding a [PlatformViewLayer] layer, +/// integrates it with the gesture arenas system and adds relevant semantic nodes to the semantics tree. +class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { /// Creating a render object for a [PlatformViewSurface]. /// /// The `controller` parameter must not be null. PlatformViewRenderBox({ @required PlatformViewController controller, - - }) : assert(controller != null && controller.viewId != null && controller.viewId > -1), - _controller = controller; + @required PlatformViewHitTestBehavior hitTestBehavior, + @required Set> gestureRecognizers, + }) : assert(controller != null && controller.viewId != null && controller.viewId > -1), + assert(hitTestBehavior != null), + assert(gestureRecognizers != null), + _controller = controller { + this.hitTestBehavior = hitTestBehavior; + updateGestureRecognizers(gestureRecognizers); + } /// Sets the [controller] for this render object. /// @@ -759,6 +729,19 @@ class PlatformViewRenderBox extends RenderBox { } } + /// How to behave during hit testing. + // The implicit setter is enough here as changing this value will just affect + // any newly arriving events there's nothing we need to invalidate. + // PlatformViewHitTestBehavior hitTestBehavior; + + /// {@macro flutter.rendering.platformView.updateGestureRecognizers} + /// + /// Any active gesture arena the `PlatformView` participates in is rejected when the + /// set of gesture recognizers is changed. + void updateGestureRecognizers(Set> gestureRecognizers) { + _updateGestureRecognizersWithCallBack(gestureRecognizers, _controller.dispatchPointerEvent); + } + PlatformViewController _controller; @override @@ -791,3 +774,56 @@ class PlatformViewRenderBox extends RenderBox { config.platformViewId = _controller.viewId; } } + +/// The Mixin handling the pointer events and gestures of a platform view render box. +mixin _PlatformViewGestureMixin on RenderBox { + + /// How to behave during hit testing. + // The implicit setter is enough here as changing this value will just affect + // any newly arriving events there's nothing we need to invalidate. + PlatformViewHitTestBehavior hitTestBehavior; + + /// {@macro flutter.rendering.platformView.updateGestureRecognizers} + /// + /// Any active gesture arena the `PlatformView` participates in is rejected when the + /// set of gesture recognizers is changed. + void _updateGestureRecognizersWithCallBack(Set> gestureRecognizers, _HandlePointerEvent _handlePointerEvent) { + assert(gestureRecognizers != null); + assert( + _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, + 'There were multiple gesture recognizer factories for the same type, there must only be a single ' + 'gesture recognizer factory for each gesture recognizer type.',); + if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { + return; + } + _gestureRecognizer?.dispose(); + _gestureRecognizer = _PlatformViewGestureRecognizer(_handlePointerEvent, gestureRecognizers); + } + + _PlatformViewGestureRecognizer _gestureRecognizer; + + @override + bool hitTest(BoxHitTestResult result, { Offset position }) { + if (hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position)) { + return false; + } + result.add(BoxHitTestEntry(this, position)); + return hitTestBehavior == PlatformViewHitTestBehavior.opaque; + } + + @override + bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent; + + @override + void handleEvent(PointerEvent event, HitTestEntry entry) { + if (event is PointerDownEvent) { + _gestureRecognizer.addPointer(event); + } + } + + @override + void detach() { + _gestureRecognizer.reset(); + super.detach(); + } +} diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 28e4ead6dd..3b43865ec2 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -7,6 +7,7 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'message_codec.dart'; import 'system_channels.dart'; @@ -725,4 +726,7 @@ abstract class PlatformViewController { /// /// See also [PlatformViewRegistry] which is a helper for managing platform view ids. int get viewId; + + /// Dispatches the `event` to the platform view. + void dispatchPointerEvent(PointerEvent event); } diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index d727fa1212..1a7b8c83ff 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -603,7 +603,11 @@ class PlatformViewSurface extends LeafRenderObjectWidget { /// The [controller] must not be null. const PlatformViewSurface({ @required this.controller, - }) : assert(controller != null); + @required this.hitTestBehavior, + @required this.gestureRecognizers, + }) : assert(controller != null), + assert(hitTestBehavior != null), + assert(gestureRecognizers != null); /// The controller for the platform view integrated by this [PlatformViewSurface]. /// @@ -611,14 +615,60 @@ class PlatformViewSurface extends LeafRenderObjectWidget { /// [PlatformViewController.viewId] identifies the platform view whose contents are painted by this widget. final PlatformViewController controller; + /// Which gestures should be forwarded to the PlatformView. + /// + /// {@macro flutter.widgets.platformViews.gestureRecognizersDescHead} + /// + /// For example, with the following setup vertical drags will not be dispatched to the platform view + /// as the vertical drag gesture is claimed by the parent [GestureDetector]. + /// + /// ```dart + /// GestureDetector( + /// onVerticalDragStart: (DragStartDetails details) {}, + /// child: PlatformViewSurface( + /// ), + /// ) + /// ``` + /// + /// To get the [PlatformViewSurface] to claim the vertical drag gestures we can pass a vertical drag + /// gesture recognizer factory in [gestureRecognizers] e.g: + /// + /// ```dart + /// GestureDetector( + /// onVerticalDragStart: (DragStartDetails details) {}, + /// child: SizedBox( + /// width: 200.0, + /// height: 100.0, + /// child: PlatformViewSurface( + /// gestureRecognizers: >[ + /// new Factory( + /// () => new EagerGestureRecognizer(), + /// ), + /// ].toSet(), + /// ), + /// ), + /// ) + /// ``` + /// + /// {@macro flutter.widgets.platformViews.gestureRecognizersDescFoot} + // We use OneSequenceGestureRecognizers as they support gesture arena teams. + // TODO(amirh): get a list of GestureRecognizers here. + // https://github.com/flutter/flutter/issues/20953 + final Set> gestureRecognizers; + + /// {@macro flutter.widgets.platformViews.hittestParam} + final PlatformViewHitTestBehavior hitTestBehavior; + @override RenderObject createRenderObject(BuildContext context) { - return PlatformViewRenderBox(controller: controller); + return PlatformViewRenderBox(controller: controller, gestureRecognizers: gestureRecognizers, hitTestBehavior: hitTestBehavior); } @override void updateRenderObject(BuildContext context, PlatformViewRenderBox renderObject) { renderObject - ..controller = controller; + ..controller = controller + ..hitTestBehavior = hitTestBehavior + ..updateGestureRecognizers(gestureRecognizers); } } diff --git a/packages/flutter/test/rendering/platform_view_test.dart b/packages/flutter/test/rendering/platform_view_test.dart index 71110c9058..763a390279 100644 --- a/packages/flutter/test/rendering/platform_view_test.dart +++ b/packages/flutter/test/rendering/platform_view_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; @@ -15,7 +17,16 @@ void main() { PlatformViewRenderBox platformViewRenderBox; setUp((){ fakePlatformViewController = FakePlatformViewController(0); - platformViewRenderBox = PlatformViewRenderBox(controller: fakePlatformViewController); + platformViewRenderBox = PlatformViewRenderBox( + controller: fakePlatformViewController, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: >{ + Factory( + () { + return VerticalDragGestureRecognizer(); + }, + ), + },); }); test('layout should size to max constraint', () { diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 9e780e9b46..d25783bece 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -8,6 +8,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; /// Used in internal testing. class FakePlatformViewController extends PlatformViewController { @@ -16,10 +17,22 @@ class FakePlatformViewController extends PlatformViewController { _id = id; } + /// Events that are dispatched; + List dispatchedPointerEvents = []; + int _id; @override int get viewId => _id; + + @override + void dispatchPointerEvent(PointerEvent event) { + dispatchedPointerEvents.add(event); + } + + void clearTestingVariables() { + dispatchedPointerEvents.clear(); + } } class FakeAndroidPlatformViewsController { diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index 0c141e52b0..e5d86532cd 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -1676,12 +1676,261 @@ void main() { }); testWidgets('PlatformViewSurface should create platform view layer', (WidgetTester tester) async { - final PlatformViewSurface surface = PlatformViewSurface(controller: controller); + final PlatformViewSurface surface = PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{},); await tester.pumpWidget(surface); final PlatformViewLayer layer = tester.layers.firstWhere((Layer layer){ return layer is PlatformViewLayer; }); expect(layer, isNotNull); }); + + testWidgets('PlatformViewSurface can lose gesture arenas', (WidgetTester tester) async { + bool verticalDragAcceptedByParent = false; + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.all(10.0), + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + verticalDragAcceptedByParent = true; + }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: PlatformViewSurface( + controller: controller, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque), + ), + ), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); + await gesture.up(); + + expect(verticalDragAcceptedByParent, true); + expect( + controller.dispatchedPointerEvents, + isEmpty, + ); + }); + + testWidgets('PlatformViewSurface gesture recognizers dispatch events', (WidgetTester tester) async { + bool verticalDragAcceptedByParent = false; + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + verticalDragAcceptedByParent = true; + }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: >{ + Factory( + () { + return VerticalDragGestureRecognizer() + ..onStart = (_) {}; // Add callback to enable recognizer + }, + ), + }, + ), + ), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); + await gesture.up(); + + expect(verticalDragAcceptedByParent, false); + expect( + controller.dispatchedPointerEvents.length, + 3, + ); + + }); + + testWidgets( + 'PlatformViewSurface can claim gesture after all pointers are up', (WidgetTester tester) async { + bool verticalDragAcceptedByParent = false; + // The long press recognizer rejects the gesture after the PlatformViewSurface gets the pointer up event. + // This test makes sure that the PlatformViewSurface can win the gesture after it got the pointer up event. + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { + verticalDragAcceptedByParent = true; + }, + onLongPress: () { }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ), + ), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(verticalDragAcceptedByParent, false); + expect( + controller.dispatchedPointerEvents.length, + 2, + ); + + }); + + testWidgets('PlatformViewSurface rebuilt during gesture', (WidgetTester tester) async { + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200.0, + height: 100.0, + child: PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); + + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200.0, + height: 100.0, + child: PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ), + ), + ), + ); + + await gesture.up(); + + expect( + controller.dispatchedPointerEvents.length, + 3, + ); + }); + + testWidgets('PlatformViewSurface with eager gesture recognizer', (WidgetTester tester) async { + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) { }, + child: SizedBox( + width: 200.0, + height: 100.0, + child: PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: >{ + Factory( + () => EagerGestureRecognizer(), + ), + }, + ), + ), + ), + ), + ); + + await tester.startGesture(const Offset(50.0, 50.0)); + + // Normally (without the eager gesture recognizer) after just the pointer down event + // no gesture arena member will claim the arena (so no motion events will be dispatched to + // the PlatformViewSurface). Here we assert that with the eager recognizer in the gesture team the + // pointer down event is immediately dispatched. + expect( + controller.dispatchedPointerEvents.length, + 1, + ); + }); + + testWidgets('PlatformViewRenderBox reconstructed with same gestureRecognizers', (WidgetTester tester) async { + + int factoryInvocationCount = 0; + final ValueGetter constructRecognizer = () { + ++ factoryInvocationCount; + return EagerGestureRecognizer(); + }; + + final PlatformViewSurface platformViewSurface = PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: >{ + Factory( + constructRecognizer, + ), + }); + + await tester.pumpWidget(platformViewSurface); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpWidget(platformViewSurface); + + expect(factoryInvocationCount, 2); + }); + + testWidgets('PlatformViewSurface rebuilt with same gestureRecognizers', (WidgetTester tester) async { + + int factoryInvocationCount = 0; + final ValueGetter constructRecognizer = () { + ++ factoryInvocationCount; + return EagerGestureRecognizer(); + }; + + await tester.pumpWidget( + PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: >{ + Factory( + constructRecognizer, + ), + }) + ); + + await tester.pumpWidget( + PlatformViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: >{ + Factory( + constructRecognizer, + ), + }) + ); + expect(factoryInvocationCount, 1); + }); }); }