diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 200b3438934..d2b3c1347be 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -107,6 +107,10 @@ class PlatformViewsService { /// The `id, `viewType, and `layoutDirection` parameters must not be null. /// If `creationParams` is non null then `creationParamsCodec` must not be null. /// {@endtemplate} + /// + /// This attempts to use the newest and most efficient platform view + /// implementation when possible. In cases where that is not supported, it + /// falls back to using Virtual Display. static AndroidViewController initAndroidView({ required int id, required String viewType, @@ -134,9 +138,10 @@ class PlatformViewsService { /// {@macro flutter.services.PlatformViewsService.initAndroidView} /// - /// Alias for [initAndroidView]. - /// This factory is provided for backward compatibility purposes. - /// In the future, this method will be deprecated. + /// This attempts to use the newest and most efficient platform view + /// implementation when possible. In cases where that is not supported, it + /// falls back to using Hybrid Composition, which is the mode used by + /// [initExpensiveAndroidView]. static SurfaceAndroidViewController initSurfaceAndroidView({ required int id, required String viewType, @@ -163,11 +168,12 @@ class PlatformViewsService { /// {@macro flutter.services.PlatformViewsService.initAndroidView} /// - /// When this factory is used, the Android view and Flutter widgets are composed at the - /// Android view hierarchy level. - /// This is only useful if the view is a Android SurfaceView. However, using this method - /// has a performance cost on devices that run below 10, or underpowered devices. - /// In most situations, you should use [initAndroidView]. + /// When this factory is used, the Android view and Flutter widgets are + /// composed at the Android view hierarchy level. + /// + /// Using this method has a performance cost on devices running Android 9 or + /// earlier, or on underpowered devices. In most situations, you should use + /// [initAndroidView] or [initSurfaceAndroidView] instead. static ExpensiveAndroidViewController initExpensiveAndroidView({ required int id, required String viewType, @@ -668,6 +674,12 @@ class _AndroidMotionEventConverter { event is! PointerDownEvent && event is! PointerUpEvent; } +class _CreationParams { + const _CreationParams(this.data, this.codec); + final dynamic data; + final MessageCodec codec; +} + /// Controls an Android view that is composed using a GL texture. /// /// Typically created with [PlatformViewsService.initAndroidView]. @@ -685,8 +697,7 @@ abstract class AndroidViewController extends PlatformViewController { assert(creationParams == null || creationParamsCodec != null), _viewType = viewType, _layoutDirection = layoutDirection, - _creationParams = creationParams, - _creationParamsCodec = creationParamsCodec; + _creationParams = creationParams == null ? null : _CreationParams(creationParams, creationParamsCodec!); /// Action code for when a primary pointer touched the screen. /// @@ -738,9 +749,7 @@ abstract class AndroidViewController extends PlatformViewController { _AndroidViewState _state = _AndroidViewState.waitingForSize; - final dynamic _creationParams; - - final MessageCodec? _creationParamsCodec; + final _CreationParams? _creationParams; final List _platformViewCreatedCallbacks = []; @@ -833,10 +842,17 @@ abstract class AndroidViewController extends PlatformViewController { /// Returns the texture entry id that the Android view is rendering into. /// - /// Returns null if the Android view has not been successfully created, or if it has been - /// disposed. + /// Returns null if the Android view has not been successfully created, if it has been + /// disposed, or if the implementation does not use textures. int? get textureId; + /// True if the view requires native view composition rather than using a + /// texture to render. + /// + /// This value may change during [create], but will not change after that + /// call's future has completed. + bool get requiresViewComposition => false; + /// Sends an Android [MotionEvent](https://developer.android.com/reference/android/view/MotionEvent) /// to the view. /// @@ -979,7 +995,7 @@ abstract class AndroidViewController extends PlatformViewController { /// Controls an Android view that is composed using a GL texture. /// This controller is created from the [PlatformViewsService.initSurfaceAndroidView] factory, /// and is defined for backward compatibility. -class SurfaceAndroidViewController extends TextureAndroidViewController{ +class SurfaceAndroidViewController extends AndroidViewController { SurfaceAndroidViewController._({ required super.viewId, required super.viewType, @@ -987,6 +1003,60 @@ class SurfaceAndroidViewController extends TextureAndroidViewController{ super.creationParams, super.creationParamsCodec, }) : super._(); + + // By default, assume the implementation will be texture-based. + _AndroidViewControllerInternals _internals = _TextureAndroidViewControllerInternals(); + + @override + bool get _createRequiresSize => true; + + @override + Future _sendCreateMessage({required Size size}) async { + assert(!size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); + + final dynamic response = await _AndroidViewControllerInternals.sendCreateMessage( + viewId: viewId, + viewType: _viewType, + hybrid: false, + hybridFallback: true, + layoutDirection: _layoutDirection, + creationParams: _creationParams, + size: size, + ); + if (response is int) { + (_internals as _TextureAndroidViewControllerInternals).textureId = response; + } else { + // A null response indicates fallback to Hybrid Composition, so swap out + // the implementation. + _internals = _HybridAndroidViewControllerInternals(); + } + return true; + } + + @override + int? get textureId { + return _internals.textureId; + } + + @override + bool get requiresViewComposition { + return _internals.requiresViewComposition; + } + + @override + Future _sendDisposeMessage() { + return _internals.sendDisposeMessage(viewId: viewId); + } + + @override + Future _sendResizeMessage(Size size) { + return _internals.setSize(size, viewId: viewId, viewState: _state); + } + + @override + Future setOffset(Offset off) { + return _internals.setOffset(off, viewId: viewId, viewState: _state); + } } /// Controls an Android view that is composed using the Android view hierarchy. @@ -1000,50 +1070,45 @@ class ExpensiveAndroidViewController extends AndroidViewController { super.creationParamsCodec, }) : super._(); + final _AndroidViewControllerInternals _internals = _HybridAndroidViewControllerInternals(); + @override bool get _createRequiresSize => false; @override Future _sendCreateMessage({required Size? size}) async { - 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, - ); - } - await SystemChannels.platform_views.invokeMethod('create', args); + await _AndroidViewControllerInternals.sendCreateMessage( + viewId: viewId, + viewType: _viewType, + hybrid: true, + layoutDirection: _layoutDirection, + creationParams: _creationParams, + ); } @override - int get textureId { - throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); + int? get textureId { + return _internals.textureId; + } + + @override + bool get requiresViewComposition { + return _internals.requiresViewComposition; } @override Future _sendDisposeMessage() { - return SystemChannels.platform_views.invokeMethod('dispose', { - 'id': viewId, - 'hybrid': true, - }); + return _internals.sendDisposeMessage(viewId: viewId); } @override Future _sendResizeMessage(Size size) { - throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); + return _internals.setSize(size, viewId: viewId, viewState: _state); } @override Future setOffset(Offset off) { - throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); + return _internals.setOffset(off, viewId: viewId, viewState: _state); } } @@ -1062,23 +1127,132 @@ class TextureAndroidViewController extends AndroidViewController { super.creationParamsCodec, }) : super._(); - /// The texture entry id into which the Android view is rendered. - int? _textureId; + final _TextureAndroidViewControllerInternals _internals = _TextureAndroidViewControllerInternals(); - /// Returns the texture entry id that the Android view is rendering into. - /// - /// Returns null if the Android view has not been successfully created, or if it has been - /// disposed. @override - int? get textureId => _textureId; + bool get _createRequiresSize => true; + + @override + Future _sendCreateMessage({required Size size}) async { + assert(!size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); + + _internals.textureId = await _AndroidViewControllerInternals.sendCreateMessage( + viewId: viewId, + viewType: _viewType, + hybrid: false, + layoutDirection: _layoutDirection, + creationParams: _creationParams, + size: size, + ) as int; + } + + @override + int? get textureId { + return _internals.textureId; + } + + @override + bool get requiresViewComposition { + return _internals.requiresViewComposition; + } + + @override + Future _sendDisposeMessage() { + return _internals.sendDisposeMessage(viewId: viewId); + } + + @override + Future _sendResizeMessage(Size size) { + return _internals.setSize(size, viewId: viewId, viewState: _state); + } + + @override + Future setOffset(Offset off) { + return _internals.setOffset(off, viewId: viewId, viewState: _state); + } +} + +// The base class for an implementation of AndroidViewController. +// +// Subclasses should correspond to different rendering modes for platform +// views, and match different mode logic on the engine side. +abstract class _AndroidViewControllerInternals { + // Sends a create message with the given parameters, and returns the result + // if any. + // + // This uses a dynamic return because depending on the mode that is selected + // on the native side, the return type is different. Callers should cast + // depending on the possible return types for their arguments. + static Future sendCreateMessage({ + required int viewId, + required String viewType, + required TextDirection layoutDirection, + required bool hybrid, + bool hybridFallback = false, + _CreationParams? creationParams, + Size? size}) { + final Map args = { + 'id': viewId, + 'viewType': viewType, + 'direction': AndroidViewController._getAndroidDirection(layoutDirection), + if (hybrid == true) 'hybrid': hybrid, + if (size != null) 'width': size.width, + if (size != null) 'height': size.height, + if (hybridFallback == true) 'hybridFallback': hybridFallback, + }; + if (creationParams != null) { + final ByteData paramsByteData = creationParams.codec.encodeMessage(creationParams.data)!; + args['params'] = Uint8List.view( + paramsByteData.buffer, + 0, + paramsByteData.lengthInBytes, + ); + } + return SystemChannels.platform_views.invokeMethod('create', args); + } + + int? get textureId; + + bool get requiresViewComposition; + + Future setSize( + Size size, { + required int viewId, + required _AndroidViewState viewState, + }); + + Future setOffset( + Offset offset, { + required int viewId, + required _AndroidViewState viewState, + }); + + Future sendDisposeMessage({required int viewId}); +} + +// An AndroidViewController implementation for views whose contents are +// displayed via a texture rather than directly in a native view. +// +// This is used for both Virtual Display and Texture Layer Hybrid Composition. +class _TextureAndroidViewControllerInternals extends _AndroidViewControllerInternals { + _TextureAndroidViewControllerInternals(); /// The current offset of the platform view. - Offset _off = Offset.zero; + Offset _offset = Offset.zero; @override - Future _sendResizeMessage(Size size) async { - assert(_state != _AndroidViewState.waitingForSize, 'Android view must have an initial size. View id: $viewId'); - assert(size != null); + int? textureId; + + @override + bool get requiresViewComposition => false; + + @override + Future setSize( + Size size, { + required int viewId, + required _AndroidViewState viewState, + }) async { + assert(viewState != _AndroidViewState.waitingForSize, 'Android view must have an initial size. View id: $viewId'); assert(!size.isEmpty); final Map? meta = await SystemChannels.platform_views.invokeMapMethod( @@ -1096,58 +1270,36 @@ class TextureAndroidViewController extends AndroidViewController { } @override - Future setOffset(Offset off) async { - if (off == _off) { + Future setOffset( + Offset offset, { + required int viewId, + required _AndroidViewState viewState, + }) async { + if (offset == _offset) { return; } // Don't set the offset unless the Android view has been created. // The implementation of this method channel throws if the Android view for this viewId // isn't addressable. - if (_state != _AndroidViewState.created) { + if (viewState != _AndroidViewState.created) { return; } - _off = off; + _offset = offset; await SystemChannels.platform_views.invokeMethod( 'offset', { 'id': viewId, - 'top': off.dy, - 'left': off.dx, + 'top': offset.dy, + 'left': offset.dx, }, ); } @override - bool get _createRequiresSize => true; - - @override - // Size is non-nullable due to _createRequiresSize returning true. - Future _sendCreateMessage({required Size size}) async { - assert(!size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); - - final Map args = { - 'id': viewId, - 'viewType': _viewType, - 'width': size.width, - 'height': size.height, - 'direction': AndroidViewController._getAndroidDirection(_layoutDirection), - }; - if (_creationParams != null) { - final ByteData paramsByteData = _creationParamsCodec!.encodeMessage(_creationParams)!; - args['params'] = Uint8List.view( - paramsByteData.buffer, - 0, - paramsByteData.lengthInBytes, - ); - } - _textureId = await SystemChannels.platform_views.invokeMethod('create', args); - } - - @override - Future _sendDisposeMessage() { + Future sendDisposeMessage({required int viewId}) { return SystemChannels .platform_views.invokeMethod('dispose', { 'id': viewId, @@ -1156,6 +1308,46 @@ class TextureAndroidViewController extends AndroidViewController { } } +// An AndroidViewController implementation for views whose contents are +// displayed directly in a native view. +// +// This is used for Hybrid Composition. +class _HybridAndroidViewControllerInternals extends _AndroidViewControllerInternals { + @override + int get textureId { + throw UnimplementedError('Not supported for hybrid composition.'); + } + + @override + bool get requiresViewComposition => true; + + @override + Future setSize( + Size size, { + required int viewId, + required _AndroidViewState viewState, + }) { + throw UnimplementedError('Not supported for hybrid composition.'); + } + + @override + Future setOffset( + Offset offset, { + required int viewId, + required _AndroidViewState viewState, + }) { + throw UnimplementedError('Not supported for hybrid composition.'); + } + + @override + Future sendDisposeMessage({required int viewId}) { + return SystemChannels.platform_views.invokeMethod('dispose', { + 'id': viewId, + 'hybrid': true, + }); + } +} + /// Controls an iOS UIView. /// /// Typically created with [PlatformViewsService.initUiKitView]. diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 6533a7245fa..f26d690e2a1 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -1070,10 +1070,79 @@ class PlatformViewSurface extends LeafRenderObjectWidget { /// /// * [AndroidView] which embeds an Android platform view in the widget hierarchy. /// * [UiKitView] which embeds an iOS platform view in the widget hierarchy. -class AndroidViewSurface extends PlatformViewSurface { +class AndroidViewSurface extends StatefulWidget { /// Construct an `AndroidPlatformViewSurface`. const AndroidViewSurface({ super.key, + required this.controller, + required this.hitTestBehavior, + required this.gestureRecognizers, + }) : assert(controller != null), + assert(hitTestBehavior != null), + assert(gestureRecognizers != null); + + /// The controller for the platform view integrated by this [AndroidViewSurface]. + /// + /// See [PlatformViewSurface.controller] for details. + final AndroidViewController controller; + + /// Which gestures should be forwarded to the PlatformView. + /// + /// See [PlatformViewSurface.gestureRecognizers] for details. + final Set> gestureRecognizers; + + /// {@macro flutter.widgets.AndroidView.hitTestBehavior} + final PlatformViewHitTestBehavior hitTestBehavior; + + @override + State createState() { + return _AndroidViewSurfaceState(); + } +} + +class _AndroidViewSurfaceState extends State { + @override + void initState() { + super.initState(); + if (!widget.controller.isCreated) { + // Schedule a rebuild once creation is complete and the final dislay + // type is known. + widget.controller.addOnPlatformViewCreatedListener(_onPlatformViewCreated); + } + } + + @override + void dispose() { + widget.controller.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.controller.requiresViewComposition) { + return _PlatformLayerBasedAndroidViewSurface( + controller: widget.controller, + hitTestBehavior: widget.hitTestBehavior, + gestureRecognizers: widget.gestureRecognizers, + ); + } else { + return _TextureBasedAndroidViewSurface( + controller: widget.controller, + hitTestBehavior: widget.hitTestBehavior, + gestureRecognizers: widget.gestureRecognizers, + ); + } + } + + void _onPlatformViewCreated(int _) { + // Trigger a re-build based on the current controller state. + setState(() {}); + } +} + +// Displays an Android platform view via GL texture. +class _TextureBasedAndroidViewSurface extends PlatformViewSurface { + const _TextureBasedAndroidViewSurface({ required AndroidViewController super.controller, required super.hitTestBehavior, required super.gestureRecognizers, @@ -1084,16 +1153,6 @@ class AndroidViewSurface extends PlatformViewSurface { @override RenderObject createRenderObject(BuildContext context) { final AndroidViewController viewController = controller as AndroidViewController; - // Compose using the Android view hierarchy. - // This is useful when embedding a SurfaceView into a Flutter app. - // SurfaceViews cannot be composed using GL textures. - if (viewController is ExpensiveAndroidViewController) { - final PlatformViewRenderBox renderBox = - super.createRenderObject(context) as PlatformViewRenderBox; - viewController.pointTransformer = - (Offset position) => renderBox.globalToLocal(position); - return renderBox; - } // Use GL texture based composition. // App should use GL texture unless they require to embed a SurfaceView. final RenderAndroidView renderBox = RenderAndroidView( @@ -1107,6 +1166,26 @@ class AndroidViewSurface extends PlatformViewSurface { } } +class _PlatformLayerBasedAndroidViewSurface extends PlatformViewSurface { + const _PlatformLayerBasedAndroidViewSurface({ + required AndroidViewController super.controller, + required super.hitTestBehavior, + required super.gestureRecognizers, + }) : assert(controller != null), + assert(hitTestBehavior != null), + assert(gestureRecognizers != null); + + @override + RenderObject createRenderObject(BuildContext context) { + final AndroidViewController viewController = controller as AndroidViewController; + final PlatformViewRenderBox renderBox = + super.createRenderObject(context) as PlatformViewRenderBox; + viewController.pointTransformer = + (Offset position) => renderBox.globalToLocal(position); + return renderBox; + } +} + /// A callback used to notify the size of the platform view placeholder. /// This size is the initial size of the platform view. typedef _OnLayoutCallback = void Function(Size size); diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 52104788023..ea728f6c36d 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -45,7 +45,11 @@ class FakePlatformViewController extends PlatformViewController { } class FakeAndroidViewController implements AndroidViewController { - FakeAndroidViewController(this.viewId, {this.requiresSize = false}); + FakeAndroidViewController( + this.viewId, { + this.requiresSize = false, + this.requiresViewComposition = false, + }); bool disposed = false; bool focusCleared = false; @@ -56,6 +60,8 @@ class FakeAndroidViewController implements AndroidViewController { bool _createCalledSuccessfully = false; + final List _createdCallbacks = []; + /// Events that are dispatched. List dispatchedPointerEvents = []; @@ -106,10 +112,13 @@ class FakeAndroidViewController implements AndroidViewController { @override void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { created = true; + createdCallbacks.add(listener); } @override - void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {} + void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { + createdCallbacks.remove(listener); + } @override Future sendMotionEvent(AndroidMotionEvent event) { @@ -128,7 +137,10 @@ class FakeAndroidViewController implements AndroidViewController { } @override - List get createdCallbacks => []; + List get createdCallbacks => _createdCallbacks; + + @override + bool requiresViewComposition; } class FakeAndroidPlatformViewsController { @@ -153,6 +165,11 @@ class FakeAndroidPlatformViewsController { Map offsets = {}; + /// True if Texture Layer Hybrid Composition mode should be enabled. + /// + /// When false, `create` will simulate the engine's fallback mode. + bool allowTextureLayerMode = true; + void registerViewType(String viewType) { _registeredViewTypes.add(viewType); } @@ -192,6 +209,7 @@ class FakeAndroidPlatformViewsController { final double? height = args['height'] as double?; final int layoutDirection = args['direction'] as int; final bool? hybrid = args['hybrid'] as bool?; + final bool? hybridFallback = args['hybridFallback'] as bool?; final Uint8List? creationParams = args['params'] as Uint8List?; if (_views.containsKey(id)) { @@ -215,11 +233,21 @@ class FakeAndroidPlatformViewsController { _views[id] = FakeAndroidPlatformView(id, viewType, width != null && height != null ? Size(width, height) : null, layoutDirection, - hybrid, - creationParams, + hybrid: hybrid, + hybridFallback: hybridFallback, + creationParams: creationParams, ); + // Return a hybrid result (null rather than a texture ID) if: + final bool hybridResult = + // hybrid was explicitly requested, or + (hybrid ?? false) || + // hybrid fallback was requested and simulated. + (!allowTextureLayerMode && (hybridFallback ?? false)); + if (hybridResult) { + return Future.value(); + } final int textureId = _textureCounter++; - return Future.sync(() => textureId); + return Future.value(textureId); } Future _dispose(MethodCall call) { @@ -506,7 +534,8 @@ class FakeHtmlPlatformViewsController { @immutable class FakeAndroidPlatformView { - const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, this.hybrid, [this.creationParams]); + const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, + {this.hybrid, this.hybridFallback, this.creationParams}); final int id; final String type; @@ -514,14 +543,16 @@ class FakeAndroidPlatformView { final Size? size; final int layoutDirection; final bool? hybrid; + final bool? hybridFallback; FakeAndroidPlatformView copyWith({Size? size, int? layoutDirection}) => FakeAndroidPlatformView( id, type, size ?? this.size, layoutDirection ?? this.layoutDirection, - hybrid, - creationParams, + hybrid: hybrid, + hybridFallback: hybridFallback, + creationParams: creationParams, ); @override @@ -535,6 +566,7 @@ class FakeAndroidPlatformView { && listEquals(other.creationParams, creationParams) && other.size == size && other.hybrid == hybrid + && other.hybridFallback == hybridFallback && other.layoutDirection == layoutDirection; } @@ -546,11 +578,14 @@ class FakeAndroidPlatformView { size, layoutDirection, hybrid, + hybridFallback, ); @override String toString() { - return 'FakeAndroidPlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection, hybrid: $hybrid, creationParams: $creationParams)'; + return 'FakeAndroidPlatformView(id: $id, type: $type, size: $size, ' + 'layoutDirection: $layoutDirection, hybrid: $hybrid, ' + 'hybridFallback: $hybridFallback, creationParams: $creationParams)'; } } diff --git a/packages/flutter/test/services/platform_views_test.dart b/packages/flutter/test/services/platform_views_test.dart index de64649069b..2750bfcab82 100644 --- a/packages/flutter/test/services/platform_views_test.dart +++ b/packages/flutter/test/services/platform_views_test.dart @@ -48,7 +48,7 @@ void main() { } }); - test('create Android views', () async { + test('create VD-fallback Android views', () async { viewsController.registerViewType('webview'); await PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr) .create(size: const Size(100.0, 100.0)); @@ -57,12 +57,85 @@ void main() { 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(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + const FakeAndroidPlatformView(1, 'webview', Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl), ]), ); }); + test('create HC-fallback Android views', () async { + viewsController.registerViewType('webview'); + await PlatformViewsService.initSurfaceAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr) + .create(size: const Size(100.0, 100.0)); + await PlatformViewsService.initSurfaceAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl) + .create(size: const Size(200.0, 300.0)); + expect( + viewsController.views, + unorderedEquals([ + const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, + hybridFallback: true), + const FakeAndroidPlatformView(1, 'webview', Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl, + hybridFallback: true), + ]), + ); + }); + + test('create HC-only Android views', () async { + viewsController.registerViewType('webview'); + await PlatformViewsService.initExpensiveAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr) + .create(size: const Size(100.0, 100.0)); + await PlatformViewsService.initExpensiveAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl) + .create(size: const Size(200.0, 300.0)); + expect( + viewsController.views, + unorderedEquals([ + const FakeAndroidPlatformView(0, 'webview', null, AndroidViewController.kAndroidLayoutDirectionLtr, + hybrid: true), + const FakeAndroidPlatformView(1, 'webview', null, AndroidViewController.kAndroidLayoutDirectionRtl, + hybrid: true), + ]), + ); + }); + + test('default view does not use view composition by default', () async { + viewsController.registerViewType('webview'); + final AndroidViewController controller = PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await controller.create(size: const Size(100.0, 100.0)); + expect(controller.requiresViewComposition, false); + }); + + test('default view does not use view composition in fallback mode', () async { + viewsController.registerViewType('webview'); + viewsController.allowTextureLayerMode = false; + final AndroidViewController controller = PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await controller.create(size: const Size(100.0, 100.0)); + viewsController.allowTextureLayerMode = true; + expect(controller.requiresViewComposition, false); + }); + + test('surface view does not use view composition by default', () async { + viewsController.registerViewType('webview'); + final AndroidViewController controller = PlatformViewsService.initSurfaceAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await controller.create(size: const Size(100.0, 100.0)); + expect(controller.requiresViewComposition, false); + }); + + test('surface view does uses view composition in fallback mode', () async { + viewsController.registerViewType('webview'); + viewsController.allowTextureLayerMode = false; + final AndroidViewController controller = PlatformViewsService.initSurfaceAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await controller.create(size: const Size(100.0, 100.0)); + viewsController.allowTextureLayerMode = true; + expect(controller.requiresViewComposition, true); + }); + + test('expensive view uses view composition', () async { + viewsController.registerViewType('webview'); + final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr); + await controller.create(size: const Size(100.0, 100.0)); + expect(controller.requiresViewComposition, true); + }); + test('reuse Android view id', () async { viewsController.registerViewType('webview'); final AndroidViewController controller = PlatformViewsService.initAndroidView( @@ -111,7 +184,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, null), + const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), ]), ); }); @@ -164,8 +237,8 @@ void main() { expect( viewsController.views, unorderedEquals([ - const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, null), - const FakeAndroidPlatformView(1, 'webview', Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr, null), + const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), + const FakeAndroidPlatformView(1, 'webview', Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr), ]), ); }); @@ -209,7 +282,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, null), + const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr), ]), ); }); @@ -226,7 +299,7 @@ void main() { expect( viewsController.views, unorderedEquals([ - const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl, null), + const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl), ]), ); }); diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index 7c15a7bef09..7af53294de9 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -41,7 +41,6 @@ void main() { 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -85,8 +84,7 @@ void main() { 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, - fakeView.creationParams, + creationParams: fakeView.creationParams, ), ]), ); @@ -150,7 +148,6 @@ void main() { 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -166,7 +163,6 @@ void main() { 'webview', const Size(100.0, 50.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -205,7 +201,6 @@ void main() { 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -272,7 +267,6 @@ void main() { 'webview', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -495,7 +489,6 @@ void main() { 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl, - null, ), ]), ); @@ -518,7 +511,6 @@ void main() { 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -549,7 +541,6 @@ void main() { 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl, - null, ), ]), ); @@ -575,7 +566,6 @@ void main() { 'maps', const Size(200.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, - null, ), ]), ); @@ -1257,6 +1247,63 @@ void main() { await tester.pumpWidget(surface); expect(controller.pointTransformer, isNotNull); }); + + testWidgets('AndroidViewSurface defaults to texture-based rendering', (WidgetTester tester) async { + final AndroidViewSurface surface = AndroidViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ); + await tester.pumpWidget(surface); + + expect(find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_TextureBasedAndroidViewSurface', + ), findsOneWidget); + }); + + testWidgets('AndroidViewSurface uses view-based rendering when initially required', (WidgetTester tester) async { + controller.requiresViewComposition = true; + final AndroidViewSurface surface = AndroidViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ); + await tester.pumpWidget(surface); + + expect(find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_PlatformLayerBasedAndroidViewSurface', + ), findsOneWidget); + }); + + testWidgets('AndroidViewSurface can switch to view-based rendering after creation', (WidgetTester tester) async { + final AndroidViewSurface surface = AndroidViewSurface( + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ); + await tester.pumpWidget(surface); + + expect(find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_TextureBasedAndroidViewSurface', + ), findsOneWidget); + expect(find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_PlatformLayerBasedAndroidViewSurface', + ), findsNothing); + + // Simulate a creation-time switch to view composition. + controller.requiresViewComposition = true; + for (final PlatformViewCreatedCallback callback in controller.createdCallbacks) { + callback(controller.viewId); + } + await tester.pumpWidget(surface); + + expect(find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_TextureBasedAndroidViewSurface', + ), findsNothing); + expect(find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_PlatformLayerBasedAndroidViewSurface', + ), findsOneWidget); + }); }); group('UiKitView', () {