diff --git a/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart b/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart index 04dadf53cd40..0ad2ff0c5ec7 100644 --- a/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart +++ b/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart @@ -76,9 +76,11 @@ Future main() async { expect( await driver.requestData('hierarchy'), '|-FlutterView\n' - ' |-FlutterSurfaceView\n' // Flutter UI + ' |-FlutterSurfaceView\n' // Flutter UI (hidden) + ' |-FlutterImageView\n' // Flutter UI (background surface) ' |-ViewGroup\n' // Platform View ' |-ViewGroup\n' + ' |-FlutterImageView\n' // Flutter UI (overlay surface) ); // Hide platform view. @@ -99,9 +101,11 @@ Future main() async { expect( await driver.requestData('hierarchy'), '|-FlutterView\n' - ' |-FlutterSurfaceView\n' // Flutter UI + ' |-FlutterSurfaceView\n' // Flutter UI (hidden) + ' |-FlutterImageView\n' // Flutter UI (background surface) ' |-ViewGroup\n' // Platform View ' |-ViewGroup\n' + ' |-FlutterImageView\n' // Flutter UI (overlay surface) ); }, timeout: Timeout.none); }); diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 49972dc53f3a..97a6d4f953f0 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -52,7 +52,7 @@ Set _factoriesTypeSet(Set> factories) { /// A render object for an Android view. /// -/// Requires Android API level 23 or greater. +/// Requires Android API level 20 or greater. /// /// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an /// Android [View](https://developer.android.com/reference/android/view/View). @@ -74,7 +74,7 @@ Set _factoriesTypeSet(Set> factories) { /// /// * [AndroidView] which is a widget that is used to show an Android view. /// * [PlatformViewsService] which is a service for controlling platform views. -class RenderAndroidView extends PlatformViewRenderBox { +class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { /// Creates a render object for an Android view. RenderAndroidView({ required AndroidViewController viewController, @@ -86,8 +86,7 @@ class RenderAndroidView extends PlatformViewRenderBox { assert(gestureRecognizers != null), assert(clipBehavior != null), _viewController = viewController, - _clipBehavior = clipBehavior, - super(controller: viewController, hitTestBehavior: hitTestBehavior, gestureRecognizers: gestureRecognizers) { + _clipBehavior = clipBehavior { _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); updateGestureRecognizers(gestureRecognizers); _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); @@ -102,22 +101,18 @@ class RenderAndroidView extends PlatformViewRenderBox { bool _isDisposed = false; /// The Android view controller for the Android view associated with this render object. - @override - AndroidViewController get controller => _viewController; - + AndroidViewController get viewController => _viewController; AndroidViewController _viewController; - /// Sets a new Android view controller. - @override - set controller(AndroidViewController controller) { + /// + /// `viewController` must not be null. + set viewController(AndroidViewController viewController) { assert(_viewController != null); - assert(controller != null); - if (_viewController == controller) + assert(viewController != null); + if (_viewController == viewController) return; _viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); - super.controller = controller; - _viewController = controller; - _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); + _viewController = viewController; _sizePlatformView(); if (_viewController.isCreated) { markNeedsSemanticsUpdate(); @@ -143,6 +138,26 @@ class RenderAndroidView extends PlatformViewRenderBox { markNeedsSemanticsUpdate(); } + /// {@template flutter.rendering.RenderAndroidView.updateGestureRecognizers} + /// Updates which gestures should be forwarded to the platform view. + /// + /// Gesture recognizers created by factories in this set participate in the gesture arena for each + /// pointer that was put down on the render box. If any of the recognizers on this list wins the + /// gesture arena, the entire pointer event sequence starting from the pointer down event + /// will be dispatched to the Android view. + /// + /// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type]. + /// + /// Setting a new set of gesture recognizer factories with the same [Factory.type]s as the current + /// set has no effect, because the factories' constructors would have already been called with the previous set. + /// {@endtemplate} + /// + /// Any active gesture arena the Android view participates in is rejected when the + /// set of gesture recognizers is changed. + void updateGestureRecognizers(Set> gestureRecognizers) { + _updateGestureRecognizersWithCallBack(gestureRecognizers, _viewController.dispatchPointerEvent); + } + @override bool get sizedByParent => true; @@ -167,8 +182,9 @@ class RenderAndroidView extends PlatformViewRenderBox { // Android virtual displays cannot have a zero size. // Trying to size it to 0 crashes the app, which was happening when starting the app // with a locked screen (see: https://github.com/flutter/flutter/issues/20456). - if (_state == _PlatformViewState.resizing || size.isEmpty) + if (_state == _PlatformViewState.resizing || size.isEmpty) { return; + } _state = _PlatformViewState.resizing; markNeedsPaint(); @@ -196,8 +212,7 @@ class RenderAndroidView extends PlatformViewRenderBox { void _setOffset() { SchedulerBinding.instance.addPostFrameCallback((_) async { if (!_isDisposed) { - if (attached) - await _viewController.setOffset(localToGlobal(Offset.zero)); + await _viewController.setOffset(localToGlobal(Offset.zero)); // Schedule a new post frame callback. _setOffset(); } @@ -206,7 +221,7 @@ class RenderAndroidView extends PlatformViewRenderBox { @override void paint(PaintingContext context, Offset offset) { - if (_viewController.textureId == null || _currentTextureSize == null) + if (_viewController.textureId == null) return; // As resizing the Android view happens asynchronously we don't know exactly when is a @@ -249,15 +264,14 @@ class RenderAndroidView extends PlatformViewRenderBox { context.addLayer(TextureLayer( rect: offset & _currentTextureSize!, - textureId: _viewController.textureId!, + textureId: viewController.textureId!, )); } @override - void describeSemanticsConfiguration(SemanticsConfiguration config) { - // Don't call the super implementation since `platformViewId` should - // be set only when the platform view is created, but the concept of - // a "created" platform view belongs to this subclass. + void describeSemanticsConfiguration (SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.isSemanticBoundary = true; if (_viewController.isCreated) { @@ -325,7 +339,7 @@ class RenderUiKitView extends RenderBox { // any newly arriving events there's nothing we need to invalidate. PlatformViewHitTestBehavior hitTestBehavior; - /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} + /// {@macro flutter.rendering.RenderAndroidView.updateGestureRecognizers} void updateGestureRecognizers(Set> gestureRecognizers) { assert(gestureRecognizers != null); assert( @@ -639,11 +653,11 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { PlatformViewController get controller => _controller; PlatformViewController _controller; /// This value must not be null, and setting it to a new value will result in a repaint. - set controller(covariant PlatformViewController controller) { + set controller(PlatformViewController controller) { assert(controller != null); assert(controller.viewId != null && controller.viewId > -1); - if (_controller == controller) { + if ( _controller == controller) { return; } final bool needsSemanticsUpdate = _controller.viewId != controller.viewId; @@ -654,19 +668,7 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { } } - /// {@template flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} - /// Updates which gestures should be forwarded to the platform view. - /// - /// Gesture recognizers created by factories in this set participate in the gesture arena for each - /// pointer that was put down on the render box. If any of the recognizers on this list wins the - /// gesture arena, the entire pointer event sequence starting from the pointer down event - /// will be dispatched to the Android view. - /// - /// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type]. - /// - /// Setting a new set of gesture recognizer factories with the same [Factory.type]s as the current - /// set has no effect, because the factories' constructors would have already been called with the previous set. - /// {@endtemplate} + /// {@macro flutter.rendering.RenderAndroidView.updateGestureRecognizers} /// /// Any active gesture arena the `PlatformView` participates in is rejected when the /// set of gesture recognizers is changed. @@ -698,7 +700,7 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { } @override - void describeSemanticsConfiguration(SemanticsConfiguration config) { + void describeSemanticsConfiguration (SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); assert(_controller.viewId != null); config.isSemanticBoundary = true; diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 2da064e68a36..f1f97a6fb5f1 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -76,8 +76,10 @@ class PlatformViewsService { /// The callbacks are invoked when the platform view asks to be focused. final Map _focusCallbacks = {}; - /// {@template flutter.services.PlatformViewsService.initAndroidView} - /// Creates a controller for a new Android view. + + /// Creates a [TextureAndroidViewController] for a new Android view. + /// + /// The view is created after calling [TextureAndroidViewController.setSize]. /// /// `id` is an unused unique identifier generated with [platformViewsRegistry]. /// @@ -101,8 +103,7 @@ class PlatformViewsService { /// /// The `id, `viewType, and `layoutDirection` parameters must not be null. /// If `creationParams` is non null then `creationParamsCodec` must not be null. - /// {@endtemplate} - static AndroidViewController initAndroidView({ + static TextureAndroidViewController initAndroidView({ required int id, required String viewType, required TextDirection layoutDirection, @@ -127,9 +128,32 @@ class PlatformViewsService { return controller; } - /// {@macro flutter.services.PlatformViewsService.initAndroidView} + /// Creates a [SurfaceAndroidViewController] for a new Android view. + /// + /// The view is created after calling [AndroidViewController.create]. + /// + /// `id` is an unused unique identifier generated with [platformViewsRegistry]. + /// + /// `viewType` is the identifier of the Android 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. + /// Plugins can register a platform view factory with + /// [PlatformViewRegistry#registerViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewRegistry.html#registerViewFactory-java.lang.String-io.flutter.plugin.platform.PlatformViewFactory-). + /// + /// `creationParams` will be passed as the args argument of [PlatformViewFactory#create](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#create-android.content.Context-int-java.lang.Object-) + /// + /// `creationParamsCodec` is the codec used to encode `creationParams` before sending it to the + /// platform side. It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-). + /// This is typically one of: [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec]. + /// + /// `onFocus` is a callback that will be invoked when the Android View asks to get the + /// input focus. /// - /// Alias for [initAndroidView]. When possible, use [initAndroidView] directly. + /// The Android view will only be created after [AndroidViewController.setSize] is called for the + /// first time. + /// + /// The `id, `viewType, and `layoutDirection` parameters must not be null. + /// If `creationParams` is non null then `creationParamsCodec` must not be null. static SurfaceAndroidViewController initSurfaceAndroidView({ required int id, required String viewType, @@ -165,11 +189,13 @@ class PlatformViewsService { /// This flag allows disabling this conversion. /// /// Defaults to true. - @Deprecated( - 'No longer necessary to improve performance. ' - 'This feature was deprecated after v2.11.0-0.1.pre.', - ) - static Future synchronizeToNativeViewHierarchy(bool yes) async {} + static Future synchronizeToNativeViewHierarchy(bool yes) { + assert(defaultTargetPlatform == TargetPlatform.android); + return SystemChannels.platform_views.invokeMethod( + 'synchronizeToNativeViewHierarchy', + yes, + ); + } // 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. @@ -650,6 +676,7 @@ abstract class AndroidViewController extends PlatformViewController { required TextDirection layoutDirection, dynamic creationParams, MessageCodec? creationParamsCodec, + bool waitingForSize = false, }) : assert(viewId != null), assert(viewType != null), assert(layoutDirection != null), @@ -657,7 +684,10 @@ abstract class AndroidViewController extends PlatformViewController { _viewType = viewType, _layoutDirection = layoutDirection, _creationParams = creationParams, - _creationParamsCodec = creationParamsCodec; + _creationParamsCodec = creationParamsCodec, + _state = waitingForSize + ? _AndroidViewState.waitingForSize + : _AndroidViewState.creating; /// Action code for when a primary pointer touched the screen. /// @@ -707,7 +737,7 @@ abstract class AndroidViewController extends PlatformViewController { TextDirection _layoutDirection; - _AndroidViewState _state = _AndroidViewState.waitingForSize; + _AndroidViewState _state; final dynamic _creationParams; @@ -818,16 +848,10 @@ abstract class AndroidViewController extends PlatformViewController { /// Removes a callback added with [addOnPlatformViewCreatedListener]. void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { - assert(listener != null); assert(_state != _AndroidViewState.disposed); _platformViewCreatedCallbacks.remove(listener); } - /// The created callbacks that are invoked after the platform view has been - /// created. - @visibleForTesting - List get createdCallbacks => _platformViewCreatedCallbacks; - /// Sets the layout direction for the Android view. Future setLayoutDirection(TextDirection layoutDirection) async { assert( @@ -916,11 +940,9 @@ abstract class AndroidViewController extends PlatformViewController { /// Controls an Android view by rendering to an [AndroidViewSurface]. /// -/// Typically created with [PlatformViewsService.initSurfaceAndroidView]. -/// -/// This is an alias for [TextureAndroidViewController]. -class SurfaceAndroidViewController extends TextureAndroidViewController{ - SurfaceAndroidViewController._({ +/// Typically created with [PlatformViewsService.initAndroidView]. +class SurfaceAndroidViewController extends AndroidViewController { + SurfaceAndroidViewController._({ required int viewId, required String viewType, required TextDirection layoutDirection, @@ -933,6 +955,50 @@ class SurfaceAndroidViewController extends TextureAndroidViewController{ creationParams: creationParams, creationParamsCodec: creationParamsCodec, ); + + @override + Future _sendCreateMessage() { + final Map args = { + 'id': viewId, + 'viewType': _viewType, + 'direction': AndroidViewController._getAndroidDirection(_layoutDirection), + 'hybrid': true, + }; + if (_creationParams != null) { + final ByteData paramsByteData = + _creationParamsCodec!.encodeMessage(_creationParams)!; + args['params'] = Uint8List.view( + paramsByteData.buffer, + 0, + paramsByteData.lengthInBytes, + ); + } + return SystemChannels.platform_views.invokeMethod('create', args); + } + + @override + int get textureId { + throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); + } + + @override + Future _sendDisposeMessage() { + return SystemChannels.platform_views + .invokeMethod('dispose', { + 'id': viewId, + 'hybrid': true, + }); + } + + @override + Future setSize(Size size) { + throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); + } + + @override + Future setOffset(Offset off) { + throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); + } } /// Controls an Android view that is rendered to a texture. @@ -953,6 +1019,7 @@ class TextureAndroidViewController extends AndroidViewController { layoutDirection: layoutDirection, creationParams: creationParams, creationParamsCodec: creationParamsCodec, + waitingForSize: true, ); /// The texture entry id into which the Android view is rendered. @@ -965,8 +1032,7 @@ class TextureAndroidViewController extends AndroidViewController { @override int? get textureId => _textureId; - /// The size used to create the platform view. - Size? _initialSize; + late Size _initialSize; /// The current offset of the platform view. Offset _off = Offset.zero; @@ -981,7 +1047,7 @@ class TextureAndroidViewController extends AndroidViewController { if (_state == _AndroidViewState.waitingForSize) { _initialSize = size; await create(); - return size; + return _initialSize; } final Map? meta = await SystemChannels.platform_views.invokeMapMethod( @@ -1027,21 +1093,17 @@ class TextureAndroidViewController extends AndroidViewController { /// /// Throws an [AssertionError] if view was already disposed. @override - Future create() async { - if (_initialSize != null) - return super.create(); - } + Future create() => super.create(); @override Future _sendCreateMessage() async { - assert(_initialSize != null, 'trying to create $TextureAndroidViewController without setting an initial size.'); - assert(!_initialSize!.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); + assert(!_initialSize.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); final Map args = { 'id': viewId, 'viewType': _viewType, - 'width': _initialSize!.width, - 'height': _initialSize!.height, + 'width': _initialSize.width, + 'height': _initialSize.height, 'direction': AndroidViewController._getAndroidDirection(_layoutDirection), }; if (_creationParams != null) { diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index e4dffffa4b6a..efd2f8c740ba 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -15,7 +15,7 @@ import 'framework.dart'; /// Embeds an Android view in the Widget hierarchy. /// -/// Requires Android API level 23 or greater. +/// Requires Android API level 20 or greater. /// /// Embedding Android views is an expensive operation and should be avoided when a Flutter /// equivalent is possible. @@ -681,7 +681,7 @@ class _AndroidPlatformView extends LeafRenderObjectWidget { @override void updateRenderObject(BuildContext context, RenderAndroidView renderObject) { - renderObject.controller = controller; + renderObject.viewController = controller; renderObject.hitTestBehavior = hitTestBehavior; renderObject.updateGestureRecognizers(gestureRecognizers); renderObject.clipBehavior = clipBehavior; @@ -842,11 +842,15 @@ class PlatformViewLink extends StatefulWidget { class _PlatformViewLinkState extends State { int? _id; PlatformViewController? _controller; + bool _platformViewCreated = false; Widget? _surface; FocusNode? _focusNode; @override Widget build(BuildContext context) { + if (!_platformViewCreated) { + return const SizedBox.expand(); + } _surface ??= widget._surfaceFactory(context, _controller!); return Focus( focusNode: _focusNode, @@ -871,6 +875,9 @@ class _PlatformViewLinkState extends State { // The _surface has to be recreated as its controller is disposed. // Setting _surface to null will trigger its creation in build(). _surface = null; + + // We are about to create a new platform view. + _platformViewCreated = false; _initialize(); } } @@ -881,12 +888,16 @@ class _PlatformViewLinkState extends State { PlatformViewCreationParams._( id: _id!, viewType: widget.viewType, - onPlatformViewCreated: (_) {}, + onPlatformViewCreated: _onPlatformViewCreated, onFocusChanged: _handlePlatformFocusChanged, ), ); } + void _onPlatformViewCreated(int id) { + setState(() { _platformViewCreated = true; }); + } + void _handleFrameworkFocusChanged(bool isFocused) { if (!isFocused) { _controller?.clearFocus(); @@ -1009,18 +1020,18 @@ class PlatformViewSurface extends LeafRenderObjectWidget { /// Integrates an Android view with Flutter's compositor, touch, and semantics subsystems. /// -/// The compositor integration is done by adding a [TextureLayer] to the layer tree. +/// The compositor integration is done by adding a [PlatformViewLayer] to the layer tree. [PlatformViewLayer] +/// isn't supported on all platforms. Custom Flutter embedders can support +/// [PlatformViewLayer]s by implementing a SystemCompositor. /// -/// The parent of this object must provide bounded layout constraints. +/// The widget fills all available space, the parent of this object must provide bounded layout +/// constraints. /// /// If the associated platform view is not created, the [AndroidViewSurface] does not paint any contents. /// -/// When possible, you may want to use [AndroidView] directly, since it requires less boilerplate code -/// than [AndroidViewSurface], and there's no difference in performance, or other trade-off(s). -/// /// See also: /// -/// * [AndroidView] which embeds an Android platform view in the widget hierarchy. +/// * [AndroidView] which embeds an Android platform view in the widget hierarchy using a [TextureLayer]. /// * [UiKitView] which embeds an iOS platform view in the widget hierarchy. class AndroidViewSurface extends PlatformViewSurface { /// Construct an `AndroidPlatformViewSurface`. @@ -1041,22 +1052,12 @@ class AndroidViewSurface extends PlatformViewSurface { @override RenderObject createRenderObject(BuildContext context) { - final AndroidViewController viewController = controller as AndroidViewController; - final RenderAndroidView renderObject = RenderAndroidView( - viewController: viewController, - gestureRecognizers: gestureRecognizers, - hitTestBehavior: hitTestBehavior, - ); - viewController.pointTransformer = - (Offset position) => renderObject.globalToLocal(position); - return renderObject; - } + final PlatformViewRenderBox renderBox = + super.createRenderObject(context) as PlatformViewRenderBox; - @override - void updateRenderObject(BuildContext context, RenderAndroidView renderObject) { - renderObject - ..controller = controller as AndroidViewController - ..hitTestBehavior = hitTestBehavior - ..updateGestureRecognizers(gestureRecognizers); + (controller as AndroidViewController).pointTransformer = + (Offset position) => renderBox.globalToLocal(position); + + return renderBox; } } diff --git a/packages/flutter/test/rendering/platform_view_test.dart b/packages/flutter/test/rendering/platform_view_test.dart index 9ede358bd2d0..5b048cce54c1 100644 --- a/packages/flutter/test/rendering/platform_view_test.dart +++ b/packages/flutter/test/rendering/platform_view_test.dart @@ -151,38 +151,6 @@ void main() { // Passes if no crashes. }); - test('created callback is reset when controller is changed', () { - final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController(); - viewsController.registerViewType('webview'); - final AndroidViewController firstController = PlatformViewsService.initAndroidView( - id: 0, - viewType: 'webview', - layoutDirection: TextDirection.rtl, - ); - final RenderAndroidView renderBox = RenderAndroidView( - viewController: firstController, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - gestureRecognizers: >{}, - ); - layout(renderBox); - pumpFrame(phase: EnginePhase.flushSemantics); - - expect(firstController.createdCallbacks, isNotEmpty); - expect(firstController.createdCallbacks.length, 1); - - final AndroidViewController secondController = PlatformViewsService.initAndroidView( - id: 0, - viewType: 'webview', - layoutDirection: TextDirection.rtl, - ); - // Reset controller. - renderBox.controller = secondController; - - expect(firstController.createdCallbacks, isEmpty); - expect(secondController.createdCallbacks, isNotEmpty); - expect(secondController.createdCallbacks.length, 1); - }); - test('render object changed its visual appearance after texture is created', () { FakeAsync().run((FakeAsync async) { final AndroidViewController viewController = diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index ac07cd2c3919..1dc698c151d6 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -84,22 +84,23 @@ class FakeAndroidViewController implements AndroidViewController { @override Future setSize(Size size) { - return Future.value(size); + throw UnimplementedError(); } @override - Future setOffset(Offset off) async {} + Future setOffset(Offset off) { + throw UnimplementedError(); + } @override - int get textureId => 0; + int get textureId => throw UnimplementedError(); @override - bool get isCreated => created; + bool get isCreated => throw UnimplementedError(); @override - void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { - created = true; - } + void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) => + throw UnimplementedError(); @override void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { @@ -117,10 +118,9 @@ class FakeAndroidViewController implements AndroidViewController { } @override - Future create() async {} - - @override - List get createdCallbacks => []; + Future create() async { + created = true; + } } class FakeAndroidPlatformViewsController { @@ -143,6 +143,8 @@ class FakeAndroidPlatformViewsController { int? lastClearedFocusViewId; + bool synchronizeToNativeViewHierarchy = true; + Map offsets = {}; void registerViewType(String viewType) { @@ -172,6 +174,8 @@ class FakeAndroidPlatformViewsController { return _clearFocus(call); case 'offset': return _offset(call); + case 'synchronizeToNativeViewHierarchy': + return _synchronizeToNativeViewHierarchy(call); } return Future.sync(() => null); } @@ -314,6 +318,11 @@ class FakeAndroidPlatformViewsController { lastClearedFocusViewId = id; return Future.sync(() => null); } + + Future _synchronizeToNativeViewHierarchy(MethodCall call) { + synchronizeToNativeViewHierarchy = call.arguments as bool; + return Future.sync(() => null); + } } class FakeIosPlatformViewsController { diff --git a/packages/flutter/test/services/platform_views_test.dart b/packages/flutter/test/services/platform_views_test.dart index 01324add63ff..064b01225364 100644 --- a/packages/flutter/test/services/platform_views_test.dart +++ b/packages/flutter/test/services/platform_views_test.dart @@ -18,7 +18,7 @@ void main() { }); test('create Android view of unregistered type', () async { - expectLater( + expect( () { return PlatformViewsService.initAndroidView( id: 0, @@ -29,25 +29,16 @@ void main() { throwsA(isA()), ); - try { - await PlatformViewsService.initSurfaceAndroidView( - id: 0, - viewType: 'web', - layoutDirection: TextDirection.ltr, - ).create(); - } catch (e) { - expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`'); - } - - try { - await PlatformViewsService.initAndroidView( - id: 0, - viewType: 'web', - layoutDirection: TextDirection.ltr, - ).create(); - } catch (e) { - expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`'); - } + expect( + () { + return PlatformViewsService.initSurfaceAndroidView( + id: 0, + viewType: 'web', + layoutDirection: TextDirection.ltr, + ).create(); + }, + throwsA(isA()), + ); }); test('create Android views', () async { @@ -56,13 +47,13 @@ void main() { .setSize(const Size(100.0, 100.0)); await PlatformViewsService.initAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl) .setSize(const Size(200.0, 300.0)); - // This platform view isn't created until the size is set. await PlatformViewsService.initSurfaceAndroidView(id: 2, viewType: 'webview', layoutDirection: TextDirection.rtl).create(); expect( viewsController.views, unorderedEquals([ const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, null), const FakeAndroidPlatformView(1, 'webview', Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl, null), + const FakeAndroidPlatformView(2, 'webview', null, AndroidViewController.kAndroidLayoutDirectionRtl, true), ]), ); }); @@ -74,7 +65,7 @@ void main() { viewType: 'webview', layoutDirection: TextDirection.ltr, ).setSize(const Size(100.0, 100.0)); - expectLater( + expect( () => PlatformViewsService.initAndroidView( id: 0, viewType: 'web', @@ -82,6 +73,20 @@ void main() { ).setSize(const Size(100.0, 100.0)), throwsA(isA()), ); + + await PlatformViewsService.initSurfaceAndroidView( + id: 1, + viewType: 'webview', + layoutDirection: TextDirection.ltr, + ).create(); + expect( + () => PlatformViewsService.initSurfaceAndroidView( + id: 1, + viewType: 'web', + layoutDirection: TextDirection.ltr, + ).create(), + throwsA(isA()), + ); }); test('dispose Android view', () async { @@ -235,6 +240,11 @@ void main() { await viewController.setOffset(const Offset(10, 20)); expect(viewsController.offsets, equals({})); }); + + test('synchronizeToNativeViewHierarchy', () async { + await PlatformViewsService.synchronizeToNativeViewHierarchy(false); + expect(viewsController.synchronizeToNativeViewHierarchy, false); + }); }); group('iOS', () { diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index f0c01ac7012f..befb278ad1b3 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -2312,6 +2312,43 @@ void main() { expect(factoryInvocationCount, 1); }); + testWidgets( + 'PlatformViewLink Widget init, should create a SizedBox widget before onPlatformViewCreated and a PlatformViewSurface after', + (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + late int createdPlatformViewId; + + late PlatformViewCreatedCallback onPlatformViewCreatedCallBack; + + final PlatformViewLink platformViewLink = PlatformViewLink( + viewType: 'webview', + onCreatePlatformView: (PlatformViewCreationParams params) { + onPlatformViewCreatedCallBack = params.onPlatformViewCreated; + createdPlatformViewId = params.id; + return FakePlatformViewController(params.id); + }, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return PlatformViewSurface( + gestureRecognizers: const >{}, + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + ); + + await tester.pumpWidget(platformViewLink); + expect(() => tester.allWidgets.whereType().first, returnsNormally); + + onPlatformViewCreatedCallBack(createdPlatformViewId); + + await tester.pump(); + + expect(() => tester.allWidgets.whereType().first, returnsNormally); + + expect(createdPlatformViewId, currentViewId + 1); + }, + ); + testWidgets('PlatformViewLink Widget dispose', (WidgetTester tester) async { late FakePlatformViewController disposedController; final PlatformViewLink platformViewLink = PlatformViewLink(