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 { ...@@ -36,6 +36,20 @@ enum _PlatformViewState {
ready, 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. /// A render object for an Android view.
/// ///
/// Requires Android API level 20 or greater. /// Requires Android API level 20 or greater.
...@@ -48,11 +62,13 @@ enum _PlatformViewState { ...@@ -48,11 +62,13 @@ enum _PlatformViewState {
/// provide bounded layout constraints. /// provide bounded layout constraints.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// RenderAndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the /// {@template flutter.rendering.platformView.gestures}
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android /// The render object participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// view can be specified with factories in [RenderAndroidView.gestureRecognizers]. If /// platform view iff it won the arena. Specific gestures that should be dispatched to the platform
/// [RenderAndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android /// view can be specified with factories in the `gestureRecognizers` constructor parameter or
/// view iff it was not claimed by any other gesture recognizer. /// 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: /// See also:
/// * [AndroidView] which is a widget that is used to show an Android view. /// * [AndroidView] which is a widget that is used to show an Android view.
...@@ -94,7 +110,8 @@ class RenderAndroidView extends RenderBox { ...@@ -94,7 +110,8 @@ class RenderAndroidView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate. // any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior; 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 /// 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 /// 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 { ...@@ -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 /// 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. /// 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 /// Any active gesture arena the Android view participates in is rejected when the
/// set of gesture recognizers is changed. /// set of gesture recognizers is changed.
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) { void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(gestureRecognizers != null); 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)) { if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) {
return; return;
} }
...@@ -118,20 +139,6 @@ class RenderAndroidView extends RenderBox { ...@@ -118,20 +139,6 @@ class RenderAndroidView extends RenderBox {
_gestureRecognizer = _AndroidViewGestureRecognizer(_motionEventsDispatcher, gestureRecognizers); _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 @override
bool get sizedByParent => true; bool get sizedByParent => true;
...@@ -244,30 +251,36 @@ class RenderAndroidView extends RenderBox { ...@@ -244,30 +251,36 @@ class RenderAndroidView extends RenderBox {
/// ///
/// {@macro flutter.rendering.platformView.layout} /// {@macro flutter.rendering.platformView.layout}
/// ///
/// {@macro flutter.rendering.platformView.gestures}
///
/// See also: /// See also:
/// * [UiKitView] which is a widget that is used to show a UIView. /// * [UiKitView] which is a widget that is used to show a UIView.
/// * [PlatformViewsService] which is a service for controlling platform views. /// * [PlatformViewsService] which is a service for controlling platform views.
class RenderUiKitView extends RenderBox { class RenderUiKitView extends RenderBox {
/// Creates a render object for an iOS UIView. /// 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({ RenderUiKitView({
@required int viewId, @required UiKitViewController viewController,
@required this.hitTestBehavior, @required this.hitTestBehavior,
}) : assert(viewId != null), @required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : assert(viewController != null),
assert(hitTestBehavior != null), assert(hitTestBehavior != null),
_viewId = viewId; assert(gestureRecognizers != null),
_viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
/// The unique identifier of the UIView controlled by this controller. /// The unique identifier of the UIView controlled by this controller.
/// ///
/// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView /// Typically generated by [PlatformViewsRegistry.getNextPlatformViewId], the UIView
/// must have been created by calling [PlatformViewsService.initUiKitView]. /// must have been created by calling [PlatformViewsService.initUiKitView].
int get viewId => _viewId; UiKitViewController get viewController => _viewController;
int _viewId; UiKitViewController _viewController;
set viewId(int viewId) { set viewController(UiKitViewController viewId) {
assert(viewId != null); assert(viewId != null);
_viewId = viewId; _viewController = viewId;
markNeedsPaint(); markNeedsPaint();
} }
...@@ -276,6 +289,20 @@ class RenderUiKitView extends RenderBox { ...@@ -276,6 +289,20 @@ class RenderUiKitView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate. // any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior; 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 @override
bool get sizedByParent => true; bool get sizedByParent => true;
...@@ -285,6 +312,8 @@ class RenderUiKitView extends RenderBox { ...@@ -285,6 +312,8 @@ class RenderUiKitView extends RenderBox {
@override @override
bool get isRepaintBoundary => true; bool get isRepaintBoundary => true;
_UiKitViewGestureRecognizer _gestureRecognizer;
@override @override
void performResize() { void performResize() {
size = constraints.biggest; size = constraints.biggest;
...@@ -294,7 +323,7 @@ class RenderUiKitView extends RenderBox { ...@@ -294,7 +323,7 @@ class RenderUiKitView extends RenderBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
context.addLayer(PlatformViewLayer( context.addLayer(PlatformViewLayer(
rect: offset & size, rect: offset & size,
viewId: _viewId, viewId: _viewController.id,
)); ));
} }
...@@ -308,16 +337,98 @@ class RenderUiKitView extends RenderBox { ...@@ -308,16 +337,98 @@ class RenderUiKitView extends RenderBox {
@override @override
bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent; 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 { class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) { _AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) {
team = GestureArenaTeam(); team = GestureArenaTeam();
team.captain = this; team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map( _gestureRecognizers = gestureRecognizerFactories.map(
(Factory<OneSequenceGestureRecognizer> factory) { (Factory<OneSequenceGestureRecognizer> recognizerFactory) {
return factory.constructor()..team = team; return recognizerFactory.constructor()..team = team;
} },
).toSet(); ).toSet();
} }
......
...@@ -617,6 +617,13 @@ class UiKitViewController { ...@@ -617,6 +617,13 @@ class UiKitViewController {
// TODO(amirh): invoke the iOS platform views channel direction method once available. // 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. /// Disposes the view.
/// ///
/// The [UiKitViewController] object is unusable after calling this. /// The [UiKitViewController] object is unusable after calling this.
......
...@@ -26,11 +26,13 @@ import 'framework.dart'; ...@@ -26,11 +26,13 @@ import 'framework.dart';
/// constraints. /// constraints.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// AndroidView participates in Flutter's [GestureArena]s, and dispatches touch events to the /// {@template flutter.widgets.platformViews.gestures}
/// Android view iff it won the arena. Specific gestures that should be dispatched to the Android /// The widget participates in Flutter's [GestureArena]s, and dispatches touch events to the
/// view can be specified in [AndroidView.gestureRecognizers]. If /// platform view iff it won the arena. Specific gestures that should be dispatched to the platform
/// [AndroidView.gestureRecognizers] is empty, the gesture will be dispatched to the Android /// 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. /// 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). /// 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-). /// 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 { ...@@ -103,13 +105,15 @@ class AndroidView extends StatefulWidget {
/// Which gestures should be forwarded to the Android view. /// 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 /// 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 /// 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 /// 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 /// 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 /// 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]. /// view as the vertical drag gesture is claimed by the parent [GestureDetector].
...@@ -125,7 +129,7 @@ class AndroidView extends StatefulWidget { ...@@ -125,7 +129,7 @@ class AndroidView extends StatefulWidget {
/// gesture recognizer factory in [gestureRecognizers] e.g: /// gesture recognizer factory in [gestureRecognizers] e.g:
/// ```dart /// ```dart
/// GestureDetector( /// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {}, /// onVerticalDragStart: (DragStartDetails details) {},
/// child: SizedBox( /// child: SizedBox(
/// width: 200.0, /// width: 200.0,
/// height: 100.0, /// height: 100.0,
...@@ -141,7 +145,8 @@ class AndroidView extends StatefulWidget { ...@@ -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]. /// by passing a factory for an [EagerGestureRecognizer] in [gestureRecognizers].
/// [EagerGestureRecognizer] is a special gesture recognizer that immediately claims the gesture /// [EagerGestureRecognizer] is a special gesture recognizer that immediately claims the gesture
/// after a pointer down event. /// after a pointer down event.
...@@ -149,7 +154,8 @@ class AndroidView extends StatefulWidget { ...@@ -149,7 +154,8 @@ class AndroidView extends StatefulWidget {
/// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type]. /// 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 /// 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. // We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here. // TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953 // https://github.com/flutter/flutter/issues/20953
...@@ -180,7 +186,12 @@ class AndroidView extends StatefulWidget { ...@@ -180,7 +186,12 @@ class AndroidView extends StatefulWidget {
/// ///
/// {@macro flutter.widgets.platformViews.layout} /// {@macro flutter.widgets.platformViews.layout}
/// ///
/// {@macro flutter.widgets.platformViews.gestures}
///
/// {@macro flutter.widgets.platformViews.lifetime} /// {@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 { class UiKitView extends StatefulWidget {
/// Creates a widget that embeds an iOS view. /// Creates a widget that embeds an iOS view.
/// ///
...@@ -194,6 +205,7 @@ class UiKitView extends StatefulWidget { ...@@ -194,6 +205,7 @@ class UiKitView extends StatefulWidget {
this.layoutDirection, this.layoutDirection,
this.creationParams, this.creationParams,
this.creationParamsCodec, this.creationParamsCodec,
this.gestureRecognizers,
}) : assert(viewType != null), }) : assert(viewType != null),
assert(hitTestBehavior != null), assert(hitTestBehavior != null),
assert(creationParams == null || creationParamsCodec != null), assert(creationParams == null || creationParamsCodec != null),
...@@ -227,6 +239,46 @@ class UiKitView extends StatefulWidget { ...@@ -227,6 +239,46 @@ class UiKitView extends StatefulWidget {
/// This must not be null if [creationParams] is not null. /// This must not be null if [creationParams] is not null.
final MessageCodec<dynamic> creationParamsCodec; 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 @override
State<UiKitView> createState() => _UiKitViewState(); State<UiKitView> createState() => _UiKitViewState();
} }
...@@ -316,7 +368,6 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -316,7 +368,6 @@ class _AndroidViewState extends State<AndroidView> {
} }
class _UiKitViewState extends State<UiKitView> { class _UiKitViewState extends State<UiKitView> {
int _id;
UiKitViewController _controller; UiKitViewController _controller;
TextDirection _layoutDirection; TextDirection _layoutDirection;
bool _initialized = false; bool _initialized = false;
...@@ -330,8 +381,9 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -330,8 +381,9 @@ class _UiKitViewState extends State<UiKitView> {
return const SizedBox.expand(); return const SizedBox.expand();
} }
return _UiKitPlatformView( return _UiKitPlatformView(
viewId: _id, controller: _controller,
hitTestBehavior: widget.hitTestBehavior, hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _emptyRecognizersSet,
); );
} }
...@@ -389,9 +441,9 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -389,9 +441,9 @@ class _UiKitViewState extends State<UiKitView> {
} }
Future<void> _createNewUiKitView() async { Future<void> _createNewUiKitView() async {
_id = platformViewsRegistry.getNextPlatformViewId(); final int id = platformViewsRegistry.getNextPlatformViewId();
final UiKitViewController controller = await PlatformViewsService.initUiKitView( final UiKitViewController controller = await PlatformViewsService.initUiKitView(
id: _id, id: id,
viewType: widget.viewType, viewType: widget.viewType,
layoutDirection: _layoutDirection, layoutDirection: _layoutDirection,
creationParams: widget.creationParams, creationParams: widget.creationParams,
...@@ -402,7 +454,7 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -402,7 +454,7 @@ class _UiKitViewState extends State<UiKitView> {
return; return;
} }
if (widget.onPlatformViewCreated != null) { if (widget.onPlatformViewCreated != null) {
widget.onPlatformViewCreated(_id); widget.onPlatformViewCreated(id);
} }
setState(() { _controller = controller; }); setState(() { _controller = controller; });
} }
...@@ -442,26 +494,31 @@ class _AndroidPlatformView extends LeafRenderObjectWidget { ...@@ -442,26 +494,31 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
class _UiKitPlatformView extends LeafRenderObjectWidget { class _UiKitPlatformView extends LeafRenderObjectWidget {
const _UiKitPlatformView({ const _UiKitPlatformView({
Key key, Key key,
@required this.viewId, @required this.controller,
@required this.hitTestBehavior, @required this.hitTestBehavior,
}) : assert(viewId != null), @required this.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null), assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
super(key: key); super(key: key);
final int viewId; final UiKitViewController controller;
final PlatformViewHitTestBehavior hitTestBehavior; final PlatformViewHitTestBehavior hitTestBehavior;
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return RenderUiKitView( return RenderUiKitView(
viewId: viewId, viewController: controller,
hitTestBehavior: hitTestBehavior, hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
); );
} }
@override @override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) { void updateRenderObject(BuildContext context, RenderUiKitView renderObject) {
renderObject.viewId = viewId; renderObject.viewController = controller;
renderObject.hitTestBehavior = hitTestBehavior; renderObject.hitTestBehavior = hitTestBehavior;
renderObject.updateGestureRecognizers(gestureRecognizers);
} }
} }
...@@ -159,6 +159,9 @@ class FakeIosPlatformViewsController { ...@@ -159,6 +159,9 @@ class FakeIosPlatformViewsController {
// delayed until it completes. // delayed until it completes.
Completer<void> creationDelay; 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) { void registerViewType(String viewType) {
_registeredViewTypes.add(viewType); _registeredViewTypes.add(viewType);
} }
...@@ -169,9 +172,11 @@ class FakeIosPlatformViewsController { ...@@ -169,9 +172,11 @@ class FakeIosPlatformViewsController {
return _create(call); return _create(call);
case 'dispose': case 'dispose':
return _dispose(call); return _dispose(call);
case 'acceptGesture':
return _acceptGesture(call);
} }
return Future<dynamic>.sync(() => null); return Future<dynamic>.sync(() => null);
} }
Future<dynamic> _create(MethodCall call) async { Future<dynamic> _create(MethodCall call) async {
if (creationDelay != null) if (creationDelay != null)
...@@ -196,6 +201,14 @@ class FakeIosPlatformViewsController { ...@@ -196,6 +201,14 @@ class FakeIosPlatformViewsController {
} }
_views[id] = FakeUiKitView(id, viewType, creationParams); _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); return Future<int>.sync(() => null);
} }
......
...@@ -999,5 +999,400 @@ void main() { ...@@ -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