Unverified Commit dc4bf652 authored by Amir Hardon's avatar Amir Hardon Committed by GitHub

Make UiKitViews participate in gesture arenas (#24027)

parent 0b953f92
......@@ -36,6 +36,20 @@ enum _PlatformViewState {
ready,
}
bool _factoryTypesSetEquals<T>(Set<Factory<T>> a, Set<Factory<T>> b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return setEquals(_factoriesTypeSet(a), _factoriesTypeSet(b));
}
Set<Type> _factoriesTypeSet<T>(Set<Factory<T>> factories) {
return factories.map<Type>((Factory<T> factory) => factory.type).toSet();
}
/// A render object for an Android view.
///
/// Requires Android API level 20 or greater.
......@@ -48,11 +62,13 @@ enum _PlatformViewState {
/// provide bounded layout constraints.
/// {@endtemplate}
///
/// RenderAndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
/// view can be specified with factories in [RenderAndroidView.gestureRecognizers]. If
/// [RenderAndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android
/// view iff it was not claimed by any other gesture recognizer.
/// {@template flutter.rendering.platformView.gestures}
/// The render object participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// platform view iff it won the arena. Specific gestures that should be dispatched to the platform
/// view can be specified with factories in the `gestureRecognizers` constructor parameter or
/// by calling `updateGestureRecognizers`. If the set of gesture recognizers is empty, the gesture
/// will be dispatched to the platform view iff it was not claimed by any other gesture recognizer.
/// {@endtemplate}
///
/// See also:
/// * [AndroidView] which is a widget that is used to show an Android view.
......@@ -94,7 +110,8 @@ class RenderAndroidView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior;
/// Which gestures should be forwarded to the Android view.
/// {@template flutter.rendering.platformView.updateGestureRecognizers}
/// Updates which gestures should be forwarded to the platform view.
///
/// Gesture recognizers created by factories in this set participate in the gesture arena for each
/// pointer that was put down on the render box. If any of the recognizers on this list wins the
......@@ -105,12 +122,16 @@ class RenderAndroidView extends RenderBox {
///
/// Setting a new set of gesture recognizer factories with the same [Factory.type]s as the current
/// set has no effect, because the factories' constructors would have already been called with the previous set.
/// {@endtemplate}
///
/// Any active gesture arena the Android view participates in is rejected when the
/// set of gesture recognizers is changed.
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(gestureRecognizers != null);
assert(_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length);
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;
}
......@@ -118,20 +139,6 @@ class RenderAndroidView extends RenderBox {
_gestureRecognizer = _AndroidViewGestureRecognizer(_motionEventsDispatcher, gestureRecognizers);
}
static bool _factoryTypesSetEquals<T>(Set<Factory<T>> a, Set<Factory<T>> b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return setEquals(_factoriesTypeSet(a), _factoriesTypeSet(b));
}
static Set<Type> _factoriesTypeSet<T>(Set<Factory<T>> factories) {
return factories.map<Type>((Factory<T> factory) => factory.type).toSet();
}
@override
bool get sizedByParent => true;
......@@ -244,30 +251,36 @@ class RenderAndroidView extends RenderBox {
///
/// {@macro flutter.rendering.platformView.layout}
///
/// {@macro flutter.rendering.platformView.gestures}
///
/// See also:
/// * [UiKitView] which is a widget that is used to show a UIView.
/// * [PlatformViewsService] which is a service for controlling platform views.
class RenderUiKitView extends RenderBox {
/// Creates a render object for an iOS UIView.
///
/// The `viewId` and `hitTestBehavior` parameters must not be null.
/// The `viewId`, `hitTestBehavior`, and `gestureRecognizers` parameters must not be null.
RenderUiKitView({
@required int viewId,
@required UiKitViewController viewController,
@required this.hitTestBehavior,
}) : assert(viewId != null),
@required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : assert(viewController != null),
assert(hitTestBehavior != null),
_viewId = viewId;
assert(gestureRecognizers != null),
_viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
/// The unique identifier of the UIView controlled by this controller.
///
/// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView
/// must have been created by calling [PlatformViewsService.initUiKitView].
int get viewId => _viewId;
int _viewId;
set viewId(int viewId) {
UiKitViewController get viewController => _viewController;
UiKitViewController _viewController;
set viewController(UiKitViewController viewId) {
assert(viewId != null);
_viewId = viewId;
_viewController = viewId;
markNeedsPaint();
}
......@@ -276,6 +289,20 @@ class RenderUiKitView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior;
/// {@macro flutter.rendering.platformView.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> 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 = _UiKitViewGestureRecognizer(viewController, gestureRecognizers);
}
@override
bool get sizedByParent => true;
......@@ -285,6 +312,8 @@ class RenderUiKitView extends RenderBox {
@override
bool get isRepaintBoundary => true;
_UiKitViewGestureRecognizer _gestureRecognizer;
@override
void performResize() {
size = constraints.biggest;
......@@ -294,7 +323,7 @@ class RenderUiKitView extends RenderBox {
void paint(PaintingContext context, Offset offset) {
context.addLayer(PlatformViewLayer(
rect: offset & size,
viewId: _viewId,
viewId: _viewController.id,
));
}
......@@ -308,16 +337,98 @@ class RenderUiKitView extends RenderBox {
@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();
}
}
// 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 _UiKitViewGesturrRecognizer
// as the team captain.
// When the team wins a gesture the recognizer notifies the engine that it should release
// the touch sequence to the embedded UIView.
class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
_UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories) {
team = GestureArenaTeam();
team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map(
(Factory<OneSequenceGestureRecognizer> recognizerFactory) {
return recognizerFactory.constructor()..team = team;
},
).toSet();
}
// 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<Factory<OneSequenceGestureRecognizer>> gestureRecognizerFactories;
Set<OneSequenceGestureRecognizer> _gestureRecognizers;
final UiKitViewController controller;
@override
void addPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event);
}
}
@override
String get debugDescription => 'UIKit view';
@override
void didStopTrackingLastPointer(int pointer) {}
@override
void handleEvent(PointerEvent event) {
stopTrackingIfPointerNoLongerDown(event);
}
@override
void acceptGesture(int pointer) {
controller.acceptGesture();
}
@override
void rejectGesture(int pointer) {
// Currently the engine rejects the gesture when the sequence is done.
// This doesn't work well with gesture recognizers that recognize after the sequence
// has ended.
// TODO(amirh): trigger an engine gesture reject here.
// https://github.com/flutter/flutter/issues/24076
}
void reset() {
resolve(GestureDisposition.rejected);
}
}
// 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
// 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, this.gestureRecognizerFactories) {
team = GestureArenaTeam();
team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map(
(Factory<OneSequenceGestureRecognizer> factory) {
return factory.constructor()..team = team;
}
(Factory<OneSequenceGestureRecognizer> recognizerFactory) {
return recognizerFactory.constructor()..team = team;
},
).toSet();
}
......
......@@ -617,6 +617,13 @@ class UiKitViewController {
// TODO(amirh): invoke the iOS platform views channel direction method once available.
}
Future<void> acceptGesture() {
final Map<String, dynamic> args = <String, dynamic> {
'id': id,
};
return SystemChannels.platform_views.invokeMethod('acceptGesture', args);
}
/// Disposes the view.
///
/// The [UiKitViewController] object is unusable after calling this.
......
......@@ -26,11 +26,13 @@ import 'framework.dart';
/// constraints.
/// {@endtemplate}
///
/// AndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android
/// view can be specified in [AndroidView.gestureRecognizers]. If
/// [AndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android
/// {@template flutter.widgets.platformViews.gestures}
/// The widget participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// platform view iff it won the arena. Specific gestures that should be dispatched to the platform
/// view can be specified in the `gestureRecognizers` constructor parameter. If
/// the set of gesture recognizers is empty, a gesture will be dispatched to the platform
/// view iff it was not claimed by any other gesture recognizer.
/// {@endtemplate}
///
/// The Android view object is created using a [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html).
/// Plugins can register platform view factories with [PlatformViewRegistry#registerViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewRegistry.html#registerViewFactory-java.lang.String-io.flutter.plugin.platform.PlatformViewFactory-).
......@@ -103,13 +105,15 @@ class AndroidView extends StatefulWidget {
/// Which gestures should be forwarded to the Android view.
///
/// {@template flutter.widgets.platformViews.gestureRecognizersDescHead}
/// The gesture recognizers built by factories in this set participate in the gesture arena for
/// each pointer that was put down on the widget. If any of these recognizers win the
/// gesture arena, the entire pointer event sequence starting from the pointer down event
/// will be dispatched to the Android view.
/// will be dispatched to the platform view.
///
/// When null, an empty set of gesture recognizer factories is used, in which case a pointer event sequence
/// will only be dispatched to the Android view if no other member of the arena claimed it.
/// will only be dispatched to the platform view if no other member of the arena claimed it.
/// {@endtemplate}
///
/// For example, with the following setup vertical drags will not be dispatched to the Android
/// view as the vertical drag gesture is claimed by the parent [GestureDetector].
......@@ -125,7 +129,7 @@ class AndroidView extends StatefulWidget {
/// gesture recognizer factory in [gestureRecognizers] e.g:
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
......@@ -141,7 +145,8 @@ class AndroidView extends StatefulWidget {
/// )
/// ```
///
/// An [AndroidView] can be configured to consume all pointers that were put down in its bounds
/// {@template flutter.widgets.platformViews.gestureRecognizersDescFoot}
/// A platform view can be configured to consume all pointers that were put down in its bounds
/// by passing a factory for an [EagerGestureRecognizer] in [gestureRecognizers].
/// [EagerGestureRecognizer] is a special gesture recognizer that immediately claims the gesture
/// after a pointer down event.
......@@ -149,7 +154,8 @@ class AndroidView extends StatefulWidget {
/// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type].
///
/// Changing `gestureRecognizers` results in rejection of any active gesture arenas (if the
/// Android view is actively participating in an arena).
/// platform view is actively participating in an arena).
/// {@endtemplate}
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
......@@ -180,7 +186,12 @@ class AndroidView extends StatefulWidget {
///
/// {@macro flutter.widgets.platformViews.layout}
///
/// {@macro flutter.widgets.platformViews.gestures}
///
/// {@macro flutter.widgets.platformViews.lifetime}
///
/// Construction of UIViews is done asynchronously, before the UIView is ready this widget paints
/// nothing while maintaining the same layout constraints.
class UiKitView extends StatefulWidget {
/// Creates a widget that embeds an iOS view.
///
......@@ -194,6 +205,7 @@ class UiKitView extends StatefulWidget {
this.layoutDirection,
this.creationParams,
this.creationParamsCodec,
this.gestureRecognizers,
}) : assert(viewType != null),
assert(hitTestBehavior != null),
assert(creationParams == null || creationParamsCodec != null),
......@@ -227,6 +239,46 @@ class UiKitView extends StatefulWidget {
/// This must not be null if [creationParams] is not null.
final MessageCodec<dynamic> creationParamsCodec;
/// Which gestures should be forwarded to the UIKit view.
///
/// {@macro flutter.widgets.platformViews.gestureRecognizersDescHead}
///
/// For example, with the following setup vertical drags will not be dispatched to the UIKit
/// view as the vertical drag gesture is claimed by the parent [GestureDetector].
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: UiKitView(
/// viewType: 'webview',
/// ),
/// )
/// ```
/// To get the [UiKitView] 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: UiKitView(
/// viewType: 'webview',
/// gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
/// new Factory<OneSequenceGestureRecognizer>(
/// () => 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<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override
State<UiKitView> createState() => _UiKitViewState();
}
......@@ -316,7 +368,6 @@ class _AndroidViewState extends State<AndroidView> {
}
class _UiKitViewState extends State<UiKitView> {
int _id;
UiKitViewController _controller;
TextDirection _layoutDirection;
bool _initialized = false;
......@@ -330,8 +381,9 @@ class _UiKitViewState extends State<UiKitView> {
return const SizedBox.expand();
}
return _UiKitPlatformView(
viewId: _id,
controller: _controller,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
);
}
......@@ -389,9 +441,9 @@ class _UiKitViewState extends State<UiKitView> {
}
Future<void> _createNewUiKitView() async {
_id = platformViewsRegistry.getNextPlatformViewId();
final int id = platformViewsRegistry.getNextPlatformViewId();
final UiKitViewController controller = await PlatformViewsService.initUiKitView(
id: _id,
id: id,
viewType: widget.viewType,
layoutDirection: _layoutDirection,
creationParams: widget.creationParams,
......@@ -402,7 +454,7 @@ class _UiKitViewState extends State<UiKitView> {
return;
}
if (widget.onPlatformViewCreated != null) {
widget.onPlatformViewCreated(_id);
widget.onPlatformViewCreated(id);
}
setState(() { _controller = controller; });
}
......@@ -442,26 +494,31 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
class _UiKitPlatformView extends LeafRenderObjectWidget {
const _UiKitPlatformView({
Key key,
@required this.viewId,
@required this.controller,
@required this.hitTestBehavior,
}) : assert(viewId != null),
@required this.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
super(key: key);
final int viewId;
final UiKitViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior;
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderUiKitView(
viewId: viewId,
viewController: controller,
hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
);
}
@override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
renderObject.viewId = viewId;
renderObject.viewController = controller;
renderObject.hitTestBehavior = hitTestBehavior;
renderObject.updateGestureRecognizers(gestureRecognizers);
}
}
......@@ -159,6 +159,9 @@ class FakeIosPlatformViewsController {
// delayed until it completes.
Completer<void> creationDelay;
// Maps a view id to the number of gestures it accepted so fat.
final Map<int, int> gesturesAccepted = <int, int>{};
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
......@@ -169,6 +172,8 @@ class FakeIosPlatformViewsController {
return _create(call);
case 'dispose':
return _dispose(call);
case 'acceptGesture':
return _acceptGesture(call);
}
return Future<dynamic>.sync(() => null);
}
......@@ -196,6 +201,14 @@ class FakeIosPlatformViewsController {
}
_views[id] = FakeUiKitView(id, viewType, creationParams);
gesturesAccepted[id] = 0;
return Future<int>.sync(() => null);
}
Future<dynamic> _acceptGesture(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments;
final int id = args['id'];
gesturesAccepted[id] += 1;
return Future<int>.sync(() => null);
}
......
......@@ -999,5 +999,400 @@ void main() {
);
});
testWidgets('UiKitView accepts gestures', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr,),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
});
testWidgets('UiKitView transparent hit test behavior', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
int numPointerDownsOnParent = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent e) {
numPointerDownsOnParent++;
},
),
Positioned(
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.transparent,
layoutDirection: TextDirection.ltr,
),
),
),
],
),
),
);
// 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.gesturesAccepted[currentViewId + 1], 0);
expect(numPointerDownsOnParent, 1);
});
testWidgets('UiKitView translucent hit test behavior', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
int numPointerDownsOnParent = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent e) {
numPointerDownsOnParent++;
},
),
Positioned(
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.translucent,
layoutDirection: TextDirection.ltr,
),
),
),
],
),
),
);
// 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.gesturesAccepted[currentViewId + 1], 1);
expect(numPointerDownsOnParent, 1);
});
testWidgets('UiKitView opaque hit test behavior', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
int numPointerDownsOnParent = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent e) {
numPointerDownsOnParent++;
},
),
Positioned(
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
layoutDirection: TextDirection.ltr,
),
),
),
],
),
),
);
// 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.gesturesAccepted[currentViewId + 1], 1);
expect(numPointerDownsOnParent, 0);
});
testWidgets('UiKitView can lose gesture arenas', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
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: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr),
),
),
),
),
);
// 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.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, true);
expect(viewsController.gesturesAccepted[currentViewId + 1], 0);
});
testWidgets('UiKitView gesture recognizers', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
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: UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
),
].toSet(),
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// 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.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, false);
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
});
testWidgets('UiKitView can claim gesture after all pointers are up', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
bool verticalDragAcceptedByParent = false;
// The long press recognizer rejects the gesture after the AndroidView gets the pointer up event.
// This test makes sure that the Android view 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: UiKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// 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(verticalDragAcceptedByParent, false);
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
});
testWidgets('UiKitView rebuilt during gesture', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
);
// 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.moveBy(const Offset(0.0, 100.0));
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
),
);
await gesture.up();
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
});
testWidgets('UiKitView with eager gesture recognizer', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {},
child: SizedBox(
width: 200.0,
height: 100.0,
child: UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
].toSet(),
layoutDirection: TextDirection.ltr,
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
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 Android view). Here we assert that with the eager recognizer in the gesture team the
// pointer down event is immediately dispatched.
expect(viewsController.gesturesAccepted[currentViewId + 1], 1);
});
testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async {
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
int factoryInvocationCount = 0;
final ValueGetter<EagerGestureRecognizer> constructRecognizer = () {
factoryInvocationCount += 1;
return EagerGestureRecognizer();
};
await tester.pumpWidget(
UiKitView(
viewType: 'webview',
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
Factory<EagerGestureRecognizer>(constructRecognizer),
].toSet(),
layoutDirection: TextDirection.ltr,
),
);
await tester.pumpWidget(
UiKitView(
viewType: 'webview',
hitTestBehavior: PlatformViewHitTestBehavior.translucent,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
Factory<EagerGestureRecognizer>(constructRecognizer),
].toSet(),
layoutDirection: TextDirection.ltr,
),
);
expect(factoryInvocationCount, 1);
});
});
}
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