diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index f4533ea5ee..e4f1f2f54e 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -357,6 +357,41 @@ class TextureLayer extends Layer { S find(Offset regionOffset) => null; } +/// A layer that shows an embedded [UIView](https://developer.apple.com/documentation/uikit/uiview) +/// on iOS. +class PlatformViewLayer extends Layer { + /// Creates a platform view layer. + /// + /// The `rect` and `viewId` parameters must not be null. + PlatformViewLayer({ + @required this.rect, + @required this.viewId, + }): assert(rect != null), assert(viewId != null); + + /// Bounding rectangle of this layer in the global coordinate space. + final Rect rect; + + /// The unique identifier of the UIView displayed on this layer. + /// + /// A UIView with this identifier must have been created by [PlatformViewsServices.initUiKitView]. + final int viewId; + + @override + ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) { + final Rect shiftedRect = rect.shift(layerOffset); + builder.addPlatformView( + viewId, + offset: shiftedRect.topLeft, + width: shiftedRect.width, + height: shiftedRect.height, + ); + return null; + } + + @override + S find(Offset regionOffset) => null; +} + /// A layer that indicates to the compositor that it should display /// certain performance statistics within it. /// diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 1734c01f92..3e4303b287 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -43,8 +43,10 @@ enum _PlatformViewState { /// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an /// Android [View](https://developer.android.com/reference/android/view/View). /// +/// {@template flutter.rendering.platformView.layout} /// The render object's layout behavior is to fill all available space, the parent of this object must /// 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 @@ -233,6 +235,81 @@ class RenderAndroidView extends RenderBox { } } +/// This is work in progress, not yet ready to be used, and requires a custom engine build. A render object for an iOS UIKit UIView. +/// +/// [RenderUiKitView] is responsible for sizing and displaying an iOS +/// [UIView](https://developer.apple.com/documentation/uikit/uiview). +/// +/// UIViews are added as sub views of the FlutterView and are composited by Quartz. +/// +/// {@macro flutter.rendering.platformView.layout} +/// +/// 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. + RenderUiKitView({ + @required int viewId, + @required this.hitTestBehavior, + }) : assert(viewId != null), + assert(hitTestBehavior != null), + _viewId = viewId; + + + /// 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) { + assert(viewId != null); + _viewId = viewId; + markNeedsPaint(); + } + + /// 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; + + @override + bool get sizedByParent => true; + + @override + bool get alwaysNeedsCompositing => true; + + @override + bool get isRepaintBoundary => true; + + @override + void performResize() { + size = constraints.biggest; + } + + @override + void paint(PaintingContext context, Offset offset) { + context.addLayer(PlatformViewLayer( + rect: offset & size, + viewId: _viewId, + )); + } + + @override + bool hitTest(HitTestResult 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; +} + class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { _AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) { team = GestureArenaTeam(); diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index b32f182fe6..de700003e2 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -91,6 +91,46 @@ class PlatformViewsService { onPlatformViewCreated, ); } + + // TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands. + /// This is work in progress, not yet ready to be used, and requires a custom engine build. Creates a controller for a new iOS UIView. + /// + /// `id` is an unused unique identifier generated with [platformViewsRegistry]. + /// + /// `viewType` is the identifier of the iOS view type to be created, a + /// factory for this view type must have been registered on the platform side. + /// Platform view factories are typically registered by plugin code. + /// + /// The `id, `viewType, and `layoutDirection` parameters must not be null. + /// If `creationParams` is non null then `cretaionParamsCodec` must not be null. + static Future initUiKitView({ + @required int id, + @required String viewType, + @required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec creationParamsCodec, + }) async { + assert(id != null); + assert(viewType != null); + assert(layoutDirection != null); + assert(creationParams == null || creationParamsCodec != null); + + // TODO(amirh): pass layoutDirection once the system channel supports it. + final Map args = { + 'id': id, + 'viewType': viewType, + }; + if (creationParams != null) { + final ByteData paramsByteData = creationParamsCodec.encodeMessage(creationParams); + args['params'] = Uint8List.view( + paramsByteData.buffer, + 0, + paramsByteData.lengthInBytes, + ); + } + await SystemChannels.platform_views.invokeMethod('create', args); + return UiKitViewController._(id, layoutDirection); + } } /// Properties of an Android pointer. @@ -455,8 +495,7 @@ class AndroidViewController { /// /// The first time a size is set triggers the creation of the Android view. Future setSize(Size size) async { - if (_state == _AndroidViewState.disposed) - throw FlutterError('trying to size a disposed Android View. View id: $id'); + assert(_state != _AndroidViewState.disposed, 'trying to size a disposed Android View. View id: $id'); assert(size != null); assert(!size.isEmpty); @@ -473,8 +512,7 @@ class AndroidViewController { /// Sets the layout direction for the Android view. Future setLayoutDirection(TextDirection layoutDirection) async { - if (_state == _AndroidViewState.disposed) - throw FlutterError('trying to set a layout direction for a disposed Android View. View id: $id'); + assert(_state != _AndroidViewState.disposed,'trying to set a layout direction for a disposed UIView. View id: $id'); if (layoutDirection == _layoutDirection) return; @@ -544,3 +582,48 @@ class AndroidViewController { _state = _AndroidViewState.created; } } + +/// Controls an iOS UIView. +/// +/// Typically created with [PlatformViewsService.initUiKitView]. +class UiKitViewController { + UiKitViewController._( + this.id, + TextDirection layoutDirection, + ) : assert(id != null), + assert(layoutDirection != null), + _layoutDirection = layoutDirection; + + + /// The unique identifier of the iOS view controlled by this controller. + /// + /// This identifer is typically generated by [PlatformViewsRegistry.getNextPlatformViewId]. + final int id; + + bool _debugDisposed = false; + + TextDirection _layoutDirection; + + /// Sets the layout direction for the Android view. + Future setLayoutDirection(TextDirection layoutDirection) async { + assert(!_debugDisposed, 'trying to set a layout direction for a disposed Android View. View id: $id'); + + if (layoutDirection == _layoutDirection) + return; + + assert(layoutDirection != null); + _layoutDirection = layoutDirection; + + // TODO(amirh): invoke the iOS platform views channel direction method once available. + } + + /// Disposes the view. + /// + /// The [UiKitViewController] object is unusable after calling this. + /// The `id` of the platform view cannot be reused after the view is + /// disposed. + Future dispose() async { + _debugDisposed = true; + await SystemChannels.platform_views.invokeMethod('dispose', id); + } +} diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 7ad64547e5..b6b0edd02f 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -21,8 +21,10 @@ import 'framework.dart'; /// The embedded Android view is painted just like any other Flutter widget and transformations /// apply to it as well. /// -/// The widget fill all available space, the parent of this object must provide bounded layout +/// {@template flutter.widgets.platformViews.layout} +/// The widget fills all available space, the parent of this object must provide bounded layout /// 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 @@ -41,19 +43,23 @@ import 'framework.dart'; /// } /// ``` /// -/// The Android view's lifetime is the same as the lifetime of the [State] object for this widget. +/// {@template flutter.widgets.platformViews.lifetime} +/// The platform view's lifetime is the same as the lifetime of the [State] object for this widget. /// When the [State] is disposed the platform view (and auxiliary resources) are lazily /// released (some resources are immediately released and some by platform garbage collector). /// A stateful widget's state is disposed when the widget is removed from the tree or when it is /// moved within the tree. If the stateful widget has a key and it's only moved relative to its siblings, /// or it has a [GlobalKey] and it's moved within the tree, it will not be disposed. +/// {@endtemplate} class AndroidView extends StatefulWidget { /// Creates a widget that embeds an Android view. /// + /// {@template flutter.widgets.platformViews.constructorParams} /// The `viewType` and `hitTestBehavior` parameters must not be null. + /// {@endtemplate} /// If `creationParams` is not null then `creationParamsCodec` must not be null. AndroidView({ // ignore: prefer_const_constructors_in_immutables - // TODO(aam): Remove lint ignore above once dartbug.com/34297 is fixed + // TODO(aam): Remove lint ignore above once https://dartbug.com/34297 is fixed Key key, @required this.viewType, this.onPlatformViewCreated, @@ -68,25 +74,32 @@ class AndroidView extends StatefulWidget { super(key: key); /// The unique identifier for Android view type to be embedded by this widget. + /// /// A [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html) /// for this type must have been registered. /// /// See also: [AndroidView] for an example of registering a platform view factory. final String viewType; - /// Callback to invoke after the Android view has been created. + /// {@template flutter.widgets.platformViews.createdParam} + /// Callback to invoke after the platform view has been created. /// /// May be null. + /// {@endtemplate} final PlatformViewCreatedCallback onPlatformViewCreated; + /// {@template flutter.widgets.platformViews.hittestParam} /// How this widget should behave during hit testing. /// /// This defaults to [PlatformViewHitTestBehavior.opaque]. + /// {@endtemplate} final PlatformViewHitTestBehavior hitTestBehavior; + /// {@template flutter.widgets.platformViews.directionParam} /// The text direction to use for the embedded view. /// /// If this is null, the ambient [Directionality] is used instead. + /// {@endtemplate} final TextDirection layoutDirection; /// Which gestures should be forwarded to the Android view. @@ -157,7 +170,50 @@ class AndroidView extends StatefulWidget { final MessageCodec creationParamsCodec; @override - State createState() => _AndroidViewState(); + State createState() => _AndroidViewState(); +} + +// TODO(amirh): describe the embedding mechanism. +/// This is work in progress, not yet ready to be used, and requires a custom engine build. Embeds an iOS view in the Widget hierarchy. +/// +/// Embedding iOS views is an expensive operation and should be avoided when a Flutter +/// equivalent is possible. +/// +/// {@macro flutter.widgets.platformViews.layout} +/// +/// {@macro flutter.widgets.platformViews.lifetime} +class UiKitView extends StatefulWidget { + /// Creates a widget that embeds an iOS view. + /// + /// {@macro flutter.widgets.platformViews.constructorParams} + UiKitView({ // ignore: prefer_const_constructors_in_immutables + // TODO(aam): Remove lint ignore above once https://dartbug.com/34297 is fixed + Key key, + @required this.viewType, + this.onPlatformViewCreated, + this.hitTestBehavior = PlatformViewHitTestBehavior.opaque, + this.layoutDirection, + }) : assert(viewType != null), + assert(hitTestBehavior != null), + super(key: key); + + // TODO(amirh): reference the iOS API doc once avaliable. + /// The unique identifier for iOS view type to be embedded by this widget. + /// + /// A PlatformViewFactory for this type must have been registered. + final String viewType; + + /// {@macro flutter.widgets.platformViews.createdParam} + final PlatformViewCreatedCallback onPlatformViewCreated; + + /// {@macro flutter.widgets.platformViews.hittestParam} + final PlatformViewHitTestBehavior hitTestBehavior; + + /// {@macro flutter.widgets.platformViews.directionParam} + final TextDirection layoutDirection; + + @override + State createState() => _UiKitViewState(); } class _AndroidViewState extends State { @@ -183,19 +239,17 @@ class _AndroidViewState extends State { return; } _initialized = true; - _layoutDirection = _findLayoutDirection(); _createNewAndroidView(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _initializeOnce(); - final TextDirection newLayoutDirection = _findLayoutDirection(); final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection; _layoutDirection = newLayoutDirection; + _initializeOnce(); if (didChangeLayoutDirection) { // The native view will update asynchronously, in the meantime we don't want // to block the framework. (so this is intentionally not awaiting). @@ -246,6 +300,97 @@ class _AndroidViewState extends State { } } +class _UiKitViewState extends State { + int _id; + UiKitViewController _controller; + TextDirection _layoutDirection; + bool _initialized = false; + + static final Set> _emptyRecognizersSet = + Set>(); + + @override + Widget build(BuildContext context) { + if (_controller == null) { + return const SizedBox.expand(); + } + return _UiKitPlatformView( + viewId: _id, + hitTestBehavior: widget.hitTestBehavior, + ); + } + + void _initializeOnce() { + if (_initialized) { + return; + } + _initialized = true; + _createNewUiKitView(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + _initializeOnce(); + if (didChangeLayoutDirection) { + // The native view will update asynchronously, in the meantime we don't want + // to block the framework. (so this is intentionally not awaiting). + _controller?.setLayoutDirection(_layoutDirection); + } + } + + @override + void didUpdateWidget(UiKitView oldWidget) { + super.didUpdateWidget(oldWidget); + + final TextDirection newLayoutDirection = _findLayoutDirection(); + final bool didChangeLayoutDirection = _layoutDirection != newLayoutDirection; + _layoutDirection = newLayoutDirection; + + if (widget.viewType != oldWidget.viewType) { + _controller?.dispose(); + _createNewUiKitView(); + return; + } + + if (didChangeLayoutDirection) { + _controller?.setLayoutDirection(_layoutDirection); + } + } + + TextDirection _findLayoutDirection() { + assert(widget.layoutDirection != null || debugCheckHasDirectionality(context)); + return widget.layoutDirection ?? Directionality.of(context); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + Future _createNewUiKitView() async { + _id = platformViewsRegistry.getNextPlatformViewId(); + final UiKitViewController controller = await PlatformViewsService.initUiKitView( + id: _id, + viewType: widget.viewType, + layoutDirection: _layoutDirection, + ); + if (!mounted) { + controller.dispose(); + return; + } + if (widget.onPlatformViewCreated != null) { + widget.onPlatformViewCreated(_id); + } + setState(() { _controller = controller; }); + } +} + class _AndroidPlatformView extends LeafRenderObjectWidget { const _AndroidPlatformView({ Key key, @@ -276,3 +421,30 @@ class _AndroidPlatformView extends LeafRenderObjectWidget { renderObject.updateGestureRecognizers(gestureRecognizers); } } + +class _UiKitPlatformView extends LeafRenderObjectWidget { + const _UiKitPlatformView({ + Key key, + @required this.viewId, + @required this.hitTestBehavior, + }) : assert(viewId != null), + assert(hitTestBehavior != null), + super(key: key); + + final int viewId; + final PlatformViewHitTestBehavior hitTestBehavior; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderUiKitView( + viewId: viewId, + hitTestBehavior: hitTestBehavior, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderUiKitView renderObject) { + renderObject.viewId = viewId; + renderObject.hitTestBehavior = hitTestBehavior; + } +} diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 8ade917380..5c2ec1cd42 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -6,21 +6,19 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; -class FakePlatformViewsController { - FakePlatformViewsController(this.targetPlatform) : assert(targetPlatform != null) { +class FakeAndroidPlatformViewsController { + FakeAndroidPlatformViewsController() { SystemChannels.platform_views.setMockMethodCallHandler(_onMethodCall); } - final TargetPlatform targetPlatform; - Iterable get views => _views.values; - final Map _views = {}; + Iterable get views => _views.values; + final Map _views = {}; - final Map> motionEvents = >{}; + final Map> motionEvents = >{}; final Set _registeredViewTypes = Set(); @@ -33,12 +31,6 @@ class FakePlatformViewsController { } Future _onMethodCall(MethodCall call) { - if (targetPlatform == TargetPlatform.android) - return _onMethodCallAndroid(call); - return Future.sync(() => null); - } - - Future _onMethodCallAndroid(MethodCall call) { switch(call.method) { case 'create': return _create(call); @@ -75,7 +67,7 @@ class FakePlatformViewsController { message: 'Trying to create a platform view of unregistered type: $viewType', ); - _views[id] = FakePlatformView(id, viewType, Size(width, height), layoutDirection, creationParams); + _views[id] = FakeAndroidPlatformView(id, viewType, Size(width, height), layoutDirection, creationParams); final int textureId = _textureCounter++; return Future.sync(() => textureId); } @@ -129,9 +121,9 @@ class FakePlatformViewsController { } if (!motionEvents.containsKey(id)) - motionEvents[id] = []; + motionEvents[id] = []; - motionEvents[id].add(FakeMotionEvent(action, pointerIds, pointerOffsets)); + motionEvents[id].add(FakeAndroidMotionEvent(action, pointerIds, pointerOffsets)); return Future.sync(() => null); } @@ -152,9 +144,77 @@ class FakePlatformViewsController { } } -class FakePlatformView { +class FakeIosPlatformViewsController { + FakeIosPlatformViewsController() { + SystemChannels.platform_views.setMockMethodCallHandler(_onMethodCall); + } - FakePlatformView(this.id, this.type, this.size, this.layoutDirection, [this.creationParams]); + + Iterable get views => _views.values; + final Map _views = {}; + + final Set _registeredViewTypes = Set(); + + // When this completer is non null, the 'create' method channel call will be + // delayed until it completes. + Completer creationDelay; + + void registerViewType(String viewType) { + _registeredViewTypes.add(viewType); + } + + Future _onMethodCall(MethodCall call) { + switch(call.method) { + case 'create': + return _create(call); + case 'dispose': + return _dispose(call); + } + return Future.sync(() => null); + } + + Future _create(MethodCall call) async { + if (creationDelay != null) + await creationDelay.future; + final Map args = call.arguments; + final int id = args['id']; + final String viewType = args['viewType']; + + if (_views.containsKey(id)) { + throw PlatformException( + code: 'error', + message: 'Trying to create an already created platform view, view id: $id', + ); + } + + if (!_registeredViewTypes.contains(viewType)) { + throw PlatformException( + code: 'error', + message: 'Trying to create a platform view of unregistered type: $viewType', + ); + } + + _views[id] = FakeUiKitView(id, viewType); + return Future.sync(() => null); + } + + Future _dispose(MethodCall call) { + final int id = call.arguments; + + if (!_views.containsKey(id)) { + throw PlatformException( + code: 'error', + message: 'Trying to dispose a platform view with unknown id: $id', + ); + } + + _views.remove(id); + return Future.sync(() => null); + } +} + +class FakeAndroidPlatformView { + FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, [this.creationParams]); final int id; final String type; @@ -164,9 +224,9 @@ class FakePlatformView { @override bool operator ==(dynamic other) { - if (other is! FakePlatformView) + if (other.runtimeType != FakeAndroidPlatformView) return false; - final FakePlatformView typedOther = other; + final FakeAndroidPlatformView typedOther = other; return id == typedOther.id && type == typedOther.type && creationParams == typedOther.creationParams && @@ -178,12 +238,12 @@ class FakePlatformView { @override String toString() { - return 'FakePlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection, creationParams: $creationParams)'; + return 'FakeAndroidPlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection, creationParams: $creationParams)'; } } -class FakeMotionEvent { - const FakeMotionEvent(this.action, this.pointerIds, this.pointers); +class FakeAndroidMotionEvent { + const FakeAndroidMotionEvent(this.action, this.pointerIds, this.pointers); final int action; final List pointers; @@ -192,9 +252,9 @@ class FakeMotionEvent { @override bool operator ==(dynamic other) { - if (other is! FakeMotionEvent) + if (other is! FakeAndroidMotionEvent) return false; - final FakeMotionEvent typedOther = other; + final FakeAndroidMotionEvent typedOther = other; const ListEquality offsetsEq = ListEquality(); const ListEquality pointersEq = ListEquality(); return pointersEq.equals(pointerIds, typedOther.pointerIds) && @@ -207,6 +267,30 @@ class FakeMotionEvent { @override String toString() { - return 'FakeMotionEvent(action: $action, pointerIds: $pointerIds, pointers: $pointers)'; + return 'FakeAndroidMotionEvent(action: $action, pointerIds: $pointerIds, pointers: $pointers)'; + } +} + +class FakeUiKitView { + FakeUiKitView(this.id, this.type); + + final int id; + final String type; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != FakeUiKitView) + return false; + final FakeUiKitView typedOther = other; + return id == typedOther.id && + type == typedOther.type; + } + + @override + int get hashCode => hashValues(id, type); + + @override + String toString() { + return 'FakeIosPlatformView(id: $id, type: $type)'; } } diff --git a/packages/flutter/test/services/platform_views_test.dart b/packages/flutter/test/services/platform_views_test.dart index b39032a399..e7e0336818 100644 --- a/packages/flutter/test/services/platform_views_test.dart +++ b/packages/flutter/test/services/platform_views_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; import '../flutter_test_alternative.dart'; @@ -10,11 +9,11 @@ import '../flutter_test_alternative.dart'; import 'fake_platform_views.dart'; void main() { - FakePlatformViewsController viewsController; group('Android', () { + FakeAndroidPlatformViewsController viewsController; setUp(() { - viewsController = FakePlatformViewsController(TargetPlatform.android); + viewsController = FakeAndroidPlatformViewsController(); }); test('create Android view of unregistered type', () async { @@ -38,9 +37,9 @@ void main() { .setSize(const Size(200.0, 300.0)); expect( viewsController.views, - unorderedEquals([ - FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), - FakePlatformView(1, 'webview', const Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl), + unorderedEquals([ + FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + FakeAndroidPlatformView(1, 'webview', const Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl), ])); }); @@ -65,11 +64,11 @@ void main() { PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr); await viewController.setSize(const Size(200.0, 300.0)); - viewController.dispose(); + await viewController.dispose(); expect( viewsController.views, - unorderedEquals([ - FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + unorderedEquals([ + FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), ])); }); @@ -94,9 +93,9 @@ void main() { await viewController.setSize(const Size(500.0, 500.0)); expect( viewsController.views, - unorderedEquals([ - FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), - FakePlatformView(1, 'webview', const Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr), + unorderedEquals([ + FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + FakeAndroidPlatformView(1, 'webview', const Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr), ])); }); @@ -129,8 +128,8 @@ void main() { await viewController.setSize(const Size(100.0, 100.0)); expect( viewsController.views, - unorderedEquals([ - FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + unorderedEquals([ + FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), ])); }); @@ -142,9 +141,88 @@ void main() { await viewController.setLayoutDirection(TextDirection.rtl); expect( viewsController.views, - unorderedEquals([ - FakePlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl), + unorderedEquals([ + FakeAndroidPlatformView(0, 'webview', const Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl), ])); }); }); + + group('iOS', () + { + FakeIosPlatformViewsController viewsController; + setUp(() { + viewsController = FakeIosPlatformViewsController(); + }); + + test('create iOS view of unregistered type', () async { + expect( + () { + return PlatformViewsService.initUiKitView( + id: 0, + viewType: 'web', + layoutDirection: TextDirection.ltr, + ); + }, + throwsA(isInstanceOf()), + ); + }); + + test('create iOS views', () async { + viewsController.registerViewType('webview'); + await PlatformViewsService.initUiKitView( + id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await PlatformViewsService.initUiKitView( + id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl); + expect( + viewsController.views, + unorderedEquals([ + FakeUiKitView(0, 'webview'), + FakeUiKitView(1, 'webview'), + ]), + ); + }); + + test('reuse iOS view id', () async { + viewsController.registerViewType('webview'); + await PlatformViewsService.initUiKitView( + id: 0, + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ); + expect( + () => PlatformViewsService.initUiKitView( + id: 0, viewType: 'web', layoutDirection: TextDirection.ltr), + throwsA(isInstanceOf()), + ); + }); + + test('dispose iOS view', () async { + viewsController.registerViewType('webview'); + await PlatformViewsService.initUiKitView( + id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + final UiKitViewController viewController = await PlatformViewsService.initUiKitView( + id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr); + + viewController.dispose(); + expect( + viewsController.views, + unorderedEquals([ + FakeUiKitView(0, 'webview'), + ])); + }); + + test('dispose inexisting iOS view', () async { + viewsController.registerViewType('webview'); + await PlatformViewsService.initUiKitView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + final UiKitViewController viewController = await PlatformViewsService.initUiKitView( + id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr); + await viewController.dispose(); + expect( + () async { + await viewController.dispose(); + }, + throwsA(isInstanceOf()), + ); + }); + }); } diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index d1b51d8463..d82e9490b0 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -16,517 +16,418 @@ import '../services/fake_platform_views.dart'; void main() { - testWidgets('Create Android view', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); + group('AndroidView', () { + testWidgets('Create Android view', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), - ), - ), - ); - - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - }); - - testWidgets('Create Android view with params', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView( - viewType: 'webview', - layoutDirection: TextDirection.ltr, - creationParams: 'creation parameters', - creationParamsCodec: const StringCodec(), - ), - ), - ), - ); - - final FakePlatformView fakeView = viewsController.views.first; - final Uint8List rawCreationParams = fakeView.creationParams; - final ByteData byteData = ByteData.view( - rawCreationParams.buffer, - rawCreationParams.offsetInBytes, - rawCreationParams.lengthInBytes - ); - final dynamic actualParams = const StringCodec().decodeMessage(byteData); - - expect(actualParams, 'creation parameters'); - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, fakeView.creationParams) - ]), - ); - }); - - testWidgets('Zero sized Android view is not created', (WidgetTester tester) async { - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - - await tester.pumpWidget( - Center( - child: SizedBox( - width: 0.0, - height: 0.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), - ), - ), - ); - - expect( - viewsController.views, - isEmpty, - ); - }); - - testWidgets('Resize Android view', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), - ), - ), - ); - - viewsController.resizeCompleter = Completer(); - - await tester.pumpWidget( - Center( - child: SizedBox( - width: 100.0, - height: 50.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), - ), - ), - ); - - final Layer textureParentLayer = tester.layers[tester.layers.length - 2]; - expect(textureParentLayer, isInstanceOf()); - final ClipRectLayer clipRect = textureParentLayer; - expect(clipRect.clipRect, Rect.fromLTWH(0.0, 0.0, 100.0, 50.0)); - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - - viewsController.resizeCompleter.complete(); - await tester.pump(); - - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'webview', const Size(100.0, 50.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - }); - - testWidgets('Change Android view type', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - viewsController.registerViewType('maps'); - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), - ), - ), - ); - - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.ltr), - ), - ), - ); - - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 2, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - }); - - testWidgets('Dispose Android view', (WidgetTester tester) async { - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), - ), - ), - ); - - await tester.pumpWidget( - const Center( - child: SizedBox( - width: 200.0, - height: 100.0, - ), - ), - ); - - expect( - viewsController.views, - isEmpty, - ); - }); - - testWidgets('Android view survives widget tree change', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), - ), - ), - ); - - await tester.pumpWidget( - Center( - child: Container( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), - ), - ), - ), - ); - - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - }); - - testWidgets('Android view gets touch events', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - await tester.pumpWidget( - Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr,), - ), - ), - ); - - final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); - await gesture.up(); - - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - const FakeMotionEvent(AndroidViewController.kActionUp, [0], [Offset(50.0, 50.0)]), - ]), - ); - }); - - testWidgets('Android view transparent hit test behavior', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - - int numPointerDownsOnParent = 0; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Stack( - children: [ - Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (PointerDownEvent e) { numPointerDownsOnParent++; }, - ), - Positioned( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView( - viewType: 'webview', - hitTestBehavior: PlatformViewHitTestBehavior.transparent, - layoutDirection: TextDirection.ltr, - ), - ), - ), - ], - ), - ), - ); - - await tester.startGesture(const Offset(50.0, 50.0)); - - expect( - viewsController.motionEvents[currentViewId + 1], - isNull, - ); - expect( - numPointerDownsOnParent, - 1 - ); - }); - - testWidgets('Android view translucent hit test behavior', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - - int numPointerDownsOnParent = 0; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Stack( - children: [ - Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (PointerDownEvent e) { numPointerDownsOnParent++; }, - ), - Positioned( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView( - viewType: 'webview', - hitTestBehavior: PlatformViewHitTestBehavior.translucent, - layoutDirection: TextDirection.ltr, - ), - ), - ), - ], - ), - ), - ); - - await tester.startGesture(const Offset(50.0, 50.0)); - - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - ]), - ); - expect( - numPointerDownsOnParent, - 1 - ); - }); - - testWidgets('Android view opaque hit test behavior', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - - int numPointerDownsOnParent = 0; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Stack( - children: [ - Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (PointerDownEvent e) { numPointerDownsOnParent++; }, - ), - Positioned( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView( - viewType: 'webview', - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - layoutDirection: TextDirection.ltr, - ), - ), - ), - ], - ), - ), - ); - - await tester.startGesture(const Offset(50.0, 50.0)); - - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - ]), - ); - expect( - numPointerDownsOnParent, - 0 - ); - }); - - testWidgets('Android view touch events are in virtual display\'s coordinate system', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - await tester.pumpWidget( - Align( - alignment: Alignment.topLeft, - child: Container( - margin: const EdgeInsets.all(10.0), + await tester.pumpWidget( + Center( child: SizedBox( width: 200.0, height: 100.0, child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), - ), - ); + ); - final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); - await gesture.up(); + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(40.0, 40.0)]), - const FakeMotionEvent(AndroidViewController.kActionUp, [0], [Offset(40.0, 40.0)]), - ]), - ); - }); + testWidgets('Create Android view with params', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); - testWidgets('Android view directionality', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('maps'); - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.rtl), - ), - ), - ); - - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl) - ]), - ); - - await tester.pumpWidget( - Center( - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.ltr), - ), - ), - ); - - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - }); - - testWidgets('Android view ambient directionality', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('maps'); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.rtl, - child: Center( + await tester.pumpWidget( + Center( child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'maps'), + child: AndroidView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + creationParams: 'creation parameters', + creationParamsCodec: const StringCodec(), + ), ), ), - ), - ); + ); - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl) - ]), - ); + final FakeAndroidPlatformView fakeView = viewsController.views.first; + final Uint8List rawCreationParams = fakeView.creationParams; + final ByteData byteData = ByteData.view( + rawCreationParams.buffer, + rawCreationParams.offsetInBytes, + rawCreationParams.lengthInBytes + ); + final dynamic actualParams = const StringCodec().decodeMessage(byteData); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Center( + expect(actualParams, 'creation parameters'); + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr, fakeView.creationParams) + ]), + ); + }); + + testWidgets('Zero sized Android view is not created', (WidgetTester tester) async { + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 0.0, + height: 0.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgets('Resize Android view', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Center( child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView(viewType: 'maps'), + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), ), ), - ), - ); + ); - expect( - viewsController.views, - unorderedEquals([ - FakePlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr) - ]), - ); - }); + viewsController.resizeCompleter = Completer(); - testWidgets('Android view can lose gesture arenas', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - 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; }, + await tester.pumpWidget( + Center( + child: SizedBox( + width: 100.0, + height: 50.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + final Layer textureParentLayer = tester.layers[tester.layers.length - 2]; + expect(textureParentLayer, isInstanceOf()); + final ClipRectLayer clipRect = textureParentLayer; + expect(clipRect.clipRect, Rect.fromLTWH(0.0, 0.0, 100.0, 50.0)); + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + + viewsController.resizeCompleter.complete(); + await tester.pump(); + + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'webview', const Size(100.0, 50.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); + + testWidgets('Change Android view type', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 2, 'maps', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); + + testWidgets('Dispose Android view', (WidgetTester tester) async { + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + ), + ), + ); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgets('Android view survives widget tree change', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), + ), + ), + ); + + await tester.pumpWidget( + Center( + child: Container( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), + ), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'webview', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); + + testWidgets('Android view gets touch events', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr,), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect( + viewsController.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionUp, [0], [Offset(50.0, 50.0)]), + ]), + ); + }); + + testWidgets('Android view transparent hit test behavior', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + int numPointerDownsOnParent = 0; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: [ + Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + numPointerDownsOnParent++; + }, + ), + Positioned( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.transparent, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ], + ), + ), + ); + + await tester.startGesture(const Offset(50.0, 50.0)); + + expect( + viewsController.motionEvents[currentViewId + 1], + isNull, + ); + expect( + numPointerDownsOnParent, + 1 + ); + }); + + testWidgets('Android view translucent hit test behavior', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + int numPointerDownsOnParent = 0; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: [ + Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + numPointerDownsOnParent++; + }, + ), + Positioned( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.translucent, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ], + ), + ), + ); + + await tester.startGesture(const Offset(50.0, 50.0)); + + expect( + viewsController.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + ]), + ); + expect( + numPointerDownsOnParent, + 1 + ); + }); + + testWidgets('Android view opaque hit test behavior', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + int numPointerDownsOnParent = 0; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: [ + Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + numPointerDownsOnParent++; + }, + ), + Positioned( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + layoutDirection: TextDirection.ltr, + ), + ), + ), + ], + ), + ), + ); + + await tester.startGesture(const Offset(50.0, 50.0)); + + expect( + viewsController.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + ]), + ); + expect( + numPointerDownsOnParent, + 0 + ); + }); + + testWidgets('Android view touch events are in virtual display\'s coordinate system', ( + WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.all(10.0), child: SizedBox( width: 200.0, height: 100.0, @@ -534,75 +435,242 @@ void main() { ), ), ), - ), - ); + ); - final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); - await gesture.moveBy(const Offset(0.0, 100.0)); - await gesture.up(); + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); - expect(verticalDragAcceptedByParent, true); - expect( - viewsController.motionEvents[currentViewId + 1], - isNull, - ); - }); + expect( + viewsController.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(40.0, 40.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionUp, [0], [Offset(40.0, 40.0)]), + ]), + ); + }); - testWidgets('Android view gesture recognizers', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - bool verticalDragAcceptedByParent = false; - await tester.pumpWidget( - Align( - alignment: Alignment.topLeft, - child: GestureDetector( - onVerticalDragStart: (DragStartDetails d) { verticalDragAcceptedByParent = true; }, + testWidgets('Android view directionality', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + Center( child: SizedBox( width: 200.0, height: 100.0, - child: AndroidView( - viewType: 'webview', - gestureRecognizers: >[ - Factory( - () => VerticalDragGestureRecognizer(), - ), - ].toSet(), - layoutDirection: TextDirection.ltr, + child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.rtl), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionRtl) + ]), + ); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); + + testWidgets('Android view ambient directionality', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps'), ), ), ), - ), - ); + ); - 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( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionRtl) + ]), + ); - expect(verticalDragAcceptedByParent, false); - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - const FakeMotionEvent(AndroidViewController.kActionMove, [0], [Offset(50.0, 150.0)]), - const FakeMotionEvent(AndroidViewController.kActionUp, [0], [Offset(50.0, 150.0)]), - ]), - ); - }); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView(viewType: 'maps'), + ), + ), + ), + ); - testWidgets('Android view can claim gesture after all pointers are up', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - 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: () {}, + expect( + viewsController.views, + unorderedEquals([ + FakeAndroidPlatformView(currentViewId + 1, 'maps', const Size(200.0, 100.0), + AndroidViewController.kAndroidLayoutDirectionLtr) + ]), + ); + }); + + testWidgets('Android view can lose gesture arenas', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + 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: AndroidView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ), + ), + ); + + 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.motionEvents[currentViewId + 1], + isNull, + ); + }); + + testWidgets('Android view gesture recognizers', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + 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: AndroidView( + viewType: 'webview', + gestureRecognizers: >[ + Factory( + () => VerticalDragGestureRecognizer(), + ), + ].toSet(), + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + 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.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionMove, [0], [Offset(50.0, 150.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionUp, [0], [Offset(50.0, 150.0)]), + ]), + ); + }); + + testWidgets( + 'Android view can claim gesture after all pointers are up', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + 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: AndroidView( + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ), + ), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); + + expect(verticalDragAcceptedByParent, false); + expect( + viewsController.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionUp, [0], [Offset(50.0, 50.0)]), + ]), + ); + }); + + testWidgets('Android view rebuilt during gesture', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, child: SizedBox( width: 200.0, height: 100.0, @@ -612,159 +680,286 @@ void main() { ), ), ), - ), - ); + ); - final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); - await gesture.up(); + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.moveBy(const Offset(0.0, 100.0)); - expect(verticalDragAcceptedByParent, false); - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - const FakeMotionEvent(AndroidViewController.kActionUp, [0], [Offset(50.0, 50.0)]), - ]), - ); - }); - - testWidgets('Android view rebuilt during gesture', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - await tester.pumpWidget( - Align( - alignment: Alignment.topLeft, - child: SizedBox( - width: 200.0, - height: 100.0, - child: AndroidView( - viewType: 'webview', - layoutDirection: TextDirection.ltr, - ), - ), - ), - ); - - 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: AndroidView( - viewType: 'webview', - layoutDirection: TextDirection.ltr, - ), - ), - ), - ); - - await gesture.up(); - - expect( - viewsController.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - const FakeMotionEvent(AndroidViewController.kActionMove, [0], [Offset(50.0, 150.0)]), - const FakeMotionEvent(AndroidViewController.kActionUp, [0], [Offset(50.0, 150.0)]), - ]), - ); - }); - - testWidgets('Android view with eager gesture recognizer', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - await tester.pumpWidget( - Align( - alignment: Alignment.topLeft, - child: GestureDetector( - onVerticalDragStart: (DragStartDetails d) {}, + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, child: SizedBox( width: 200.0, height: 100.0, child: AndroidView( viewType: 'webview', - gestureRecognizers: >[ - Factory( - () => EagerGestureRecognizer(), - ), - ].toSet(), layoutDirection: TextDirection.ltr, ), ), ), - ), - ); + ); - await tester.startGesture(const Offset(50.0, 50.0)); + await gesture.up(); - // 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.motionEvents[currentViewId + 1], - orderedEquals([ - const FakeMotionEvent(AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), - ]), - ); - }); + expect( + viewsController.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionMove, [0], [Offset(50.0, 150.0)]), + const FakeAndroidMotionEvent( + AndroidViewController.kActionUp, [0], [Offset(50.0, 150.0)]), + ]), + ); + }); - testWidgets('RenderAndroidView reconstructed with same gestureRecognizers', (WidgetTester tester) async { - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); - - final AndroidView androidView = AndroidView( - viewType: 'webview', - gestureRecognizers: >[ - Factory( - () => EagerGestureRecognizer(), + testWidgets('Android view with eager gesture recognizer', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Align( + alignment: Alignment.topLeft, + child: GestureDetector( + onVerticalDragStart: (DragStartDetails d) {}, + child: SizedBox( + width: 200.0, + height: 100.0, + child: AndroidView( + viewType: 'webview', + gestureRecognizers: >[ + Factory( + () => EagerGestureRecognizer(), + ), + ].toSet(), + layoutDirection: TextDirection.ltr, + ), + ), + ), ), - ].toSet(), - layoutDirection: TextDirection.ltr, - ); + ); - await tester.pumpWidget(androidView); - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpWidget(androidView); - }); + await tester.startGesture(const Offset(50.0, 50.0)); - testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async { - final FakePlatformViewsController viewsController = FakePlatformViewsController(TargetPlatform.android); - viewsController.registerViewType('webview'); + // 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.motionEvents[currentViewId + 1], + orderedEquals([ + const FakeAndroidMotionEvent( + AndroidViewController.kActionDown, [0], [Offset(50.0, 50.0)]), + ]), + ); + }); - int factoryInvocationCount = 0; - final ValueGetter constructRecognizer = () { + testWidgets('RenderAndroidView reconstructed with same gestureRecognizers', ( + WidgetTester tester) async { + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + final AndroidView androidView = AndroidView( + viewType: 'webview', + gestureRecognizers: >[ + Factory( + () => EagerGestureRecognizer(), + ), + ].toSet(), + layoutDirection: TextDirection.ltr, + ); + + await tester.pumpWidget(androidView); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpWidget(androidView); + }); + + testWidgets('AndroidView rebuilt with same gestureRecognizers', (WidgetTester tester) async { + final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); + viewsController.registerViewType('webview'); + + int factoryInvocationCount = 0; + final ValueGetter constructRecognizer = () { factoryInvocationCount += 1; return EagerGestureRecognizer(); }; - await tester.pumpWidget( - AndroidView( - viewType: 'webview', - gestureRecognizers: >[ - Factory(constructRecognizer), - ].toSet(), - layoutDirection: TextDirection.ltr, - ), - ); + await tester.pumpWidget( + AndroidView( + viewType: 'webview', + gestureRecognizers: >[ + Factory(constructRecognizer), + ].toSet(), + layoutDirection: TextDirection.ltr, + ), + ); - await tester.pumpWidget( - AndroidView( - viewType: 'webview', - hitTestBehavior: PlatformViewHitTestBehavior.translucent, - gestureRecognizers: >[ - Factory(constructRecognizer), - ].toSet(), - layoutDirection: TextDirection.ltr, - ), - ); + await tester.pumpWidget( + AndroidView( + viewType: 'webview', + hitTestBehavior: PlatformViewHitTestBehavior.translucent, + gestureRecognizers: >[ + Factory(constructRecognizer), + ].toSet(), + layoutDirection: TextDirection.ltr, + ), + ); + + expect(factoryInvocationCount, 1); + }); + }); + + group('UiKitView', () { + testWidgets('Create UIView', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeUiKitView(currentViewId + 1, 'webview') + ]), + ); + }); + + testWidgets('Change UIView view type', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + viewsController.registerViewType('maps'); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'maps', layoutDirection: TextDirection.ltr), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeUiKitView(currentViewId + 2, 'maps') + ]), + ); + }); + + testWidgets('Dispose UIView ', (WidgetTester tester) async { + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + ), + ), + ); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgets('Dispose UIView before creation completed ', (WidgetTester tester) async { + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + viewsController.creationDelay = Completer(); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr), + ), + ), + ); + + await tester.pumpWidget( + const Center( + child: SizedBox( + width: 200.0, + height: 100.0, + ), + ), + ); + + viewsController.creationDelay.complete(); + + expect( + viewsController.views, + isEmpty, + ); + }); + + testWidgets('UIView survives widget tree change', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController(); + viewsController.registerViewType('webview'); + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), + ), + ), + ); + + await tester.pumpWidget( + Center( + child: Container( + child: SizedBox( + width: 200.0, + height: 100.0, + child: UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr, key: key), + ), + ), + ), + ); + + expect( + viewsController.views, + unorderedEquals([ + FakeUiKitView(currentViewId + 1, 'webview') + ]), + ); + }); - expect(factoryInvocationCount, 1); }); }