From 032205eaca9573abfe9770e0521626a8b7ab0676 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 29 Mar 2022 19:40:17 -0700 Subject: [PATCH] Reland: "Use texture layer when displaying an Android view" (#100990) --- .ci.yaml | 1 + .../android/app/src/main/AndroidManifest.xml | 7 +- .../lib/android_platform_view.dart | 24 ++- .../lib/nested_view_event_page.dart | 71 ++++++--- .../test_driver/main_test.dart | 54 ++++++- .../lib/src/rendering/platform_view.dart | 84 +++++----- .../lib/src/services/platform_views.dart | 147 ++++++++++-------- .../lib/src/widgets/platform_view.dart | 53 ++++--- .../test/rendering/platform_view_test.dart | 32 ++++ .../test/services/fake_platform_views.dart | 41 ++--- .../test/services/platform_views_test.dart | 54 +++---- .../test/widgets/platform_view_test.dart | 37 ----- 12 files changed, 342 insertions(+), 263 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index f97120d1b63..3cc18d1c96c 100755 --- a/.ci.yaml +++ b/.ci.yaml @@ -2023,6 +2023,7 @@ targets: - name: Linux_android hybrid_android_views_integration_test recipe: devicelab/devicelab_drone presubmit: false + bringup: true # https://github.com/flutter/flutter/issues/100991 timeout: 60 properties: tags: > diff --git a/dev/integration_tests/hybrid_android_views/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/hybrid_android_views/android/app/src/main/AndroidManifest.xml index 0e3e9627e49..63d3eac735e 100644 --- a/dev/integration_tests/hybrid_android_views/android/app/src/main/AndroidManifest.xml +++ b/dev/integration_tests/hybrid_android_views/android/app/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ found in the LICENSE file. --> android:launchMode="singleTop" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true"> @@ -28,9 +29,5 @@ found in the LICENSE file. --> - - diff --git a/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart b/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart index 536497506ec..40630d43339 100644 --- a/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart +++ b/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart @@ -17,6 +17,7 @@ class AndroidPlatformView extends StatelessWidget { const AndroidPlatformView({ Key? key, this.onPlatformViewCreated, + this.useHybridComposition = false, required this.viewType, }) : assert(viewType != null), super(key: key); @@ -31,6 +32,9 @@ class AndroidPlatformView extends StatelessWidget { /// May be null. final PlatformViewCreatedCallback? onPlatformViewCreated; + // Use hybrid composition. + final bool useHybridComposition; + @override Widget build(BuildContext context) { return PlatformViewLink( @@ -44,17 +48,27 @@ class AndroidPlatformView extends StatelessWidget { ); }, onCreatePlatformView: (PlatformViewCreationParams params) { - final AndroidViewController controller = - PlatformViewsService.initSurfaceAndroidView( + print('useHybridComposition=$useHybridComposition'); + late AndroidViewController controller; + if (useHybridComposition) { + controller = PlatformViewsService.initExpensiveAndroidView( id: params.id, viewType: params.viewType, layoutDirection: TextDirection.ltr, - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated); + ); + } else { + controller = PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: params.viewType, + layoutDirection: TextDirection.ltr, + ); + } if (onPlatformViewCreated != null) { controller.addOnPlatformViewCreatedListener(onPlatformViewCreated!); } - return controller..create(); + return controller + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); }, ); } diff --git a/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart b/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart index 1c894d2ee2b..b298b0a1c74 100644 --- a/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart +++ b/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart @@ -39,6 +39,7 @@ class NestedViewEventBodyState extends State { int? id; int nestedViewClickCount = 0; bool showPlatformView = true; + bool useHybridComposition = false; @override Widget build(BuildContext context) { @@ -55,39 +56,63 @@ class NestedViewEventBodyState extends State { key: const ValueKey('PlatformView'), viewType: 'simple_view', onPlatformViewCreated: onPlatformViewCreated, + useHybridComposition: useHybridComposition, ) : null, ), if (_lastTestStatus != _LastTestStatus.pending) _statusWidget(), if (viewChannel != null) ... [ - ElevatedButton( - key: const ValueKey('ShowAlertDialog'), - onPressed: onShowAlertDialogPressed, - child: const Text('SHOW ALERT DIALOG'), - ), - ElevatedButton( - key: const ValueKey('TogglePlatformView'), - onPressed: onTogglePlatformView, - child: const Text('TOGGLE PLATFORM VIEW'), + Row( + children: [ + Expanded( + child: ElevatedButton( + key: const ValueKey('ShowAlertDialog'), + onPressed: onShowAlertDialogPressed, + child: const Text('SHOW ALERT DIALOG'), + ), + ), + Expanded( + child: ElevatedButton( + key: const ValueKey('TogglePlatformView'), + onPressed: onTogglePlatformView, + child: const Text('TOGGLE PLATFORM VIEW'), + ), + ), + ], ), Row( children: [ - ElevatedButton( - key: const ValueKey('AddChildView'), - onPressed: onChildViewPressed, - child: const Text('ADD CHILD VIEW'), - ), - ElevatedButton( - key: const ValueKey('TapChildView'), - onPressed: onTapChildViewPressed, - child: const Text('TAP CHILD VIEW'), - ), - if (nestedViewClickCount > 0) - Text( - 'Click count: $nestedViewClickCount', - key: const ValueKey('NestedViewClickCount'), + Expanded( + child: ElevatedButton( + key: const ValueKey('ToggleHybridComposition'), + child: const Text('TOGGLE HC'), + onPressed: () { + setState(() { + useHybridComposition = !useHybridComposition; + }); + }, ), + ), + Expanded( + child: ElevatedButton( + key: const ValueKey('AddChildView'), + onPressed: onChildViewPressed, + child: const Text('ADD CHILD VIEW'), + ), + ), + Expanded( + child: ElevatedButton( + key: const ValueKey('TapChildView'), + onPressed: onTapChildViewPressed, + child: const Text('TAP CHILD VIEW'), + ), + ), ], ), + if (nestedViewClickCount > 0) + Text( + 'Click count: $nestedViewClickCount', + key: const ValueKey('NestedViewClickCount'), + ), ], ], ), 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 0ad2ff0c5ec..d4c75ed32af 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 @@ -59,10 +59,58 @@ Future main() async { }, timeout: Timeout.none); }); - group('Flutter surface switch', () { + group('Flutter surface without hybrid composition', () { setUpAll(() async { - final SerializableFinder wmListTile = find.byValueKey('NestedViewEventTile'); - await driver.tap(wmListTile); + await driver.tap(find.byValueKey('NestedViewEventTile')); + }); + + tearDownAll(() async { + await driver.waitFor(find.pageBack()); + await driver.tap(find.pageBack()); + }); + + test('Uses FlutterSurfaceView when Android view is on the screen', () async { + await driver.waitFor(find.byValueKey('PlatformView')); + + expect( + await driver.requestData('hierarchy'), + '|-FlutterView\n' + ' |-FlutterSurfaceView\n' // Flutter UI + ' |-ViewGroup\n' // Platform View + ' |-ViewGroup\n' + ); + + // Hide platform view. + final SerializableFinder togglePlatformView = find.byValueKey('TogglePlatformView'); + await driver.tap(togglePlatformView); + await driver.waitForAbsent(find.byValueKey('PlatformView')); + + expect( + await driver.requestData('hierarchy'), + '|-FlutterView\n' + ' |-FlutterSurfaceView\n' // Just the Flutter UI + ); + + // Show platform view again. + await driver.tap(togglePlatformView); + await driver.waitFor(find.byValueKey('PlatformView')); + + expect( + await driver.requestData('hierarchy'), + '|-FlutterView\n' + ' |-FlutterSurfaceView\n' // Flutter UI + ' |-ViewGroup\n' // Platform View + ' |-ViewGroup\n' + ); + }, timeout: Timeout.none); + }); + + group('Flutter surface with hybrid composition', () { + setUpAll(() async { + await driver.tap(find.byValueKey('NestedViewEventTile')); + await driver.tap(find.byValueKey('ToggleHybridComposition')); + await driver.tap(find.byValueKey('TogglePlatformView')); + await driver.tap(find.byValueKey('TogglePlatformView')); }); tearDownAll(() async { diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 97a6d4f953f..49972dc53f3 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 20 or greater. +/// Requires Android API level 23 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 RenderBox with _PlatformViewGestureMixin { +class RenderAndroidView extends PlatformViewRenderBox { /// Creates a render object for an Android view. RenderAndroidView({ required AndroidViewController viewController, @@ -86,7 +86,8 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { assert(gestureRecognizers != null), assert(clipBehavior != null), _viewController = viewController, - _clipBehavior = clipBehavior { + _clipBehavior = clipBehavior, + super(controller: viewController, hitTestBehavior: hitTestBehavior, gestureRecognizers: gestureRecognizers) { _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); updateGestureRecognizers(gestureRecognizers); _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); @@ -101,18 +102,22 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { bool _isDisposed = false; /// The Android view controller for the Android view associated with this render object. - AndroidViewController get viewController => _viewController; + @override + AndroidViewController get controller => _viewController; + AndroidViewController _viewController; + /// Sets a new Android view controller. - /// - /// `viewController` must not be null. - set viewController(AndroidViewController viewController) { + @override + set controller(AndroidViewController controller) { assert(_viewController != null); - assert(viewController != null); - if (_viewController == viewController) + assert(controller != null); + if (_viewController == controller) return; _viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); - _viewController = viewController; + super.controller = controller; + _viewController = controller; + _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); _sizePlatformView(); if (_viewController.isCreated) { markNeedsSemanticsUpdate(); @@ -138,26 +143,6 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { 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; @@ -182,9 +167,8 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { // 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(); @@ -212,7 +196,8 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { void _setOffset() { SchedulerBinding.instance.addPostFrameCallback((_) async { if (!_isDisposed) { - await _viewController.setOffset(localToGlobal(Offset.zero)); + if (attached) + await _viewController.setOffset(localToGlobal(Offset.zero)); // Schedule a new post frame callback. _setOffset(); } @@ -221,7 +206,7 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { @override void paint(PaintingContext context, Offset offset) { - if (_viewController.textureId == null) + if (_viewController.textureId == null || _currentTextureSize == null) return; // As resizing the Android view happens asynchronously we don't know exactly when is a @@ -264,14 +249,15 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { context.addLayer(TextureLayer( rect: offset & _currentTextureSize!, - textureId: viewController.textureId!, + textureId: _viewController.textureId!, )); } @override - void describeSemanticsConfiguration (SemanticsConfiguration config) { - super.describeSemanticsConfiguration(config); - + 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. config.isSemanticBoundary = true; if (_viewController.isCreated) { @@ -339,7 +325,7 @@ class RenderUiKitView extends RenderBox { // any newly arriving events there's nothing we need to invalidate. PlatformViewHitTestBehavior hitTestBehavior; - /// {@macro flutter.rendering.RenderAndroidView.updateGestureRecognizers} + /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} void updateGestureRecognizers(Set> gestureRecognizers) { assert(gestureRecognizers != null); assert( @@ -653,11 +639,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(PlatformViewController controller) { + set controller(covariant 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; @@ -668,7 +654,19 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { } } - /// {@macro flutter.rendering.RenderAndroidView.updateGestureRecognizers} + /// {@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} /// /// Any active gesture arena the `PlatformView` participates in is rejected when the /// set of gesture recognizers is changed. @@ -700,7 +698,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 f1f97a6fb5f..fb4c9ba9ac9 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -76,10 +76,8 @@ class PlatformViewsService { /// The callbacks are invoked when the platform view asks to be focused. final Map _focusCallbacks = {}; - - /// Creates a [TextureAndroidViewController] for a new Android view. - /// - /// The view is created after calling [TextureAndroidViewController.setSize]. + /// {@template flutter.services.PlatformViewsService.initAndroidView} + /// Creates a controller for a new Android view. /// /// `id` is an unused unique identifier generated with [platformViewsRegistry]. /// @@ -103,7 +101,8 @@ class PlatformViewsService { /// /// The `id, `viewType, and `layoutDirection` parameters must not be null. /// If `creationParams` is non null then `creationParamsCodec` must not be null. - static TextureAndroidViewController initAndroidView({ + /// {@endtemplate} + static AndroidViewController initAndroidView({ required int id, required String viewType, required TextDirection layoutDirection, @@ -128,32 +127,11 @@ class PlatformViewsService { return controller; } - /// Creates a [SurfaceAndroidViewController] for a new Android view. + /// {@macro flutter.services.PlatformViewsService.initAndroidView} /// - /// 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. - /// - /// 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. + /// Alias for [initAndroidView]. + /// This factory is provided for backward compatibility purposes. + /// In the future, this method will be deprecated. static SurfaceAndroidViewController initSurfaceAndroidView({ required int id, required String viewType, @@ -174,28 +152,43 @@ class PlatformViewsService { creationParams: creationParams, creationParamsCodec: creationParamsCodec, ); + _instance._focusCallbacks[id] = onFocus ?? () {}; + return controller; + } + + /// {@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]. + static ExpensiveAndroidViewController initExpensiveAndroidView({ + required int id, + required String viewType, + required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec? creationParamsCodec, + VoidCallback? onFocus, + }) { + final ExpensiveAndroidViewController controller = ExpensiveAndroidViewController._( + viewId: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: creationParamsCodec, + ); _instance._focusCallbacks[id] = onFocus ?? () {}; return controller; } /// Whether the render surface of the Android `FlutterView` should be converted to a `FlutterImageView`. - /// - /// When adding platform views using - /// [Hybrid Composition](https://flutter.dev/docs/development/platform-integration/platform-views), - /// the engine converts the render surface to a `FlutterImageView` to improve - /// animation synchronization between Flutter widgets and the Android platform - /// views. On Android versions < 10, this can have some performance issues. - /// This flag allows disabling this conversion. - /// - /// Defaults to true. - static Future synchronizeToNativeViewHierarchy(bool yes) { - assert(defaultTargetPlatform == TargetPlatform.android); - return SystemChannels.platform_views.invokeMethod( - 'synchronizeToNativeViewHierarchy', - yes, - ); - } + @Deprecated( + 'No longer necessary to improve performance. ' + 'This feature was deprecated after v2.11.0-0.1.pre.', + ) + static Future synchronizeToNativeViewHierarchy(bool yes) async {} // 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. @@ -665,7 +658,7 @@ class _AndroidMotionEventConverter { event is! PointerDownEvent && event is! PointerUpEvent; } -/// Controls an Android view. +/// Controls an Android view that is composed using a GL texture. /// /// Typically created with [PlatformViewsService.initAndroidView]. // TODO(bparrishMines): Remove abstract methods that are not required by all subclasses. @@ -676,7 +669,6 @@ abstract class AndroidViewController extends PlatformViewController { required TextDirection layoutDirection, dynamic creationParams, MessageCodec? creationParamsCodec, - bool waitingForSize = false, }) : assert(viewId != null), assert(viewType != null), assert(layoutDirection != null), @@ -684,10 +676,7 @@ abstract class AndroidViewController extends PlatformViewController { _viewType = viewType, _layoutDirection = layoutDirection, _creationParams = creationParams, - _creationParamsCodec = creationParamsCodec, - _state = waitingForSize - ? _AndroidViewState.waitingForSize - : _AndroidViewState.creating; + _creationParamsCodec = creationParamsCodec; /// Action code for when a primary pointer touched the screen. /// @@ -737,7 +726,7 @@ abstract class AndroidViewController extends PlatformViewController { TextDirection _layoutDirection; - _AndroidViewState _state; + _AndroidViewState _state = _AndroidViewState.waitingForSize; final dynamic _creationParams; @@ -848,10 +837,16 @@ 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( @@ -938,11 +933,29 @@ abstract class AndroidViewController extends PlatformViewController { } } -/// Controls an Android view by rendering to an [AndroidViewSurface]. -/// -/// Typically created with [PlatformViewsService.initAndroidView]. -class SurfaceAndroidViewController extends AndroidViewController { - SurfaceAndroidViewController._({ +/// 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{ + SurfaceAndroidViewController._({ + required int viewId, + required String viewType, + required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec? creationParamsCodec, + }) : super._( + viewId: viewId, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: creationParamsCodec, + ); +} + +/// Controls an Android view that is composed using the Android view hierarchy. +/// This controller is created from the [PlatformViewsService.initExpensiveAndroidView] factory. +class ExpensiveAndroidViewController extends AndroidViewController { + ExpensiveAndroidViewController._({ required int viewId, required String viewType, required TextDirection layoutDirection, @@ -1019,7 +1032,6 @@ class TextureAndroidViewController extends AndroidViewController { layoutDirection: layoutDirection, creationParams: creationParams, creationParamsCodec: creationParamsCodec, - waitingForSize: true, ); /// The texture entry id into which the Android view is rendered. @@ -1032,7 +1044,8 @@ class TextureAndroidViewController extends AndroidViewController { @override int? get textureId => _textureId; - late Size _initialSize; + /// The size used to create the platform view. + Size? _initialSize; /// The current offset of the platform view. Offset _off = Offset.zero; @@ -1047,7 +1060,7 @@ class TextureAndroidViewController extends AndroidViewController { if (_state == _AndroidViewState.waitingForSize) { _initialSize = size; await create(); - return _initialSize; + return size; } final Map? meta = await SystemChannels.platform_views.invokeMapMethod( @@ -1093,17 +1106,21 @@ class TextureAndroidViewController extends AndroidViewController { /// /// Throws an [AssertionError] if view was already disposed. @override - Future create() => super.create(); + Future create() async { + if (_initialSize != null) + return super.create(); + } @override Future _sendCreateMessage() async { - assert(!_initialSize.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); + assert(_initialSize != null, 'trying to create $TextureAndroidViewController without setting an initial 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 efd2f8c740b..cdcf74f49da 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 20 or greater. +/// Requires Android API level 23 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.viewController = controller; + renderObject.controller = controller; renderObject.hitTestBehavior = hitTestBehavior; renderObject.updateGestureRecognizers(gestureRecognizers); renderObject.clipBehavior = clipBehavior; @@ -842,15 +842,11 @@ 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, @@ -875,9 +871,6 @@ 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(); } } @@ -888,16 +881,12 @@ 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(); @@ -1020,18 +1009,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 [PlatformViewLayer] to the layer tree. [PlatformViewLayer] -/// isn't supported on all platforms. Custom Flutter embedders can support -/// [PlatformViewLayer]s by implementing a SystemCompositor. +/// The compositor integration is done by adding a [TextureLayer] to the layer tree. /// -/// The widget fills all available space, the parent of this object must provide bounded layout -/// constraints. +/// 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 using a [TextureLayer]. +/// * [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 { /// Construct an `AndroidPlatformViewSurface`. @@ -1052,12 +1041,26 @@ class AndroidViewSurface extends PlatformViewSurface { @override RenderObject createRenderObject(BuildContext context) { - final PlatformViewRenderBox renderBox = - super.createRenderObject(context) as PlatformViewRenderBox; - - (controller as AndroidViewController).pointTransformer = + 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( + viewController: viewController, + gestureRecognizers: gestureRecognizers, + hitTestBehavior: hitTestBehavior, + ); + viewController.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 5b048cce54c..9ede358bd2d 100644 --- a/packages/flutter/test/rendering/platform_view_test.dart +++ b/packages/flutter/test/rendering/platform_view_test.dart @@ -151,6 +151,38 @@ 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 1dc698c151d..ac07cd2c391 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -84,24 +84,23 @@ class FakeAndroidViewController implements AndroidViewController { @override Future setSize(Size size) { - throw UnimplementedError(); + return Future.value(size); } @override - Future setOffset(Offset off) { - throw UnimplementedError(); + Future setOffset(Offset off) async {} + + @override + int get textureId => 0; + + @override + bool get isCreated => created; + + @override + void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { + created = true; } - @override - int get textureId => throw UnimplementedError(); - - @override - bool get isCreated => throw UnimplementedError(); - - @override - void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) => - throw UnimplementedError(); - @override void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { throw UnimplementedError(); @@ -118,9 +117,10 @@ class FakeAndroidViewController implements AndroidViewController { } @override - Future create() async { - created = true; - } + Future create() async {} + + @override + List get createdCallbacks => []; } class FakeAndroidPlatformViewsController { @@ -143,8 +143,6 @@ class FakeAndroidPlatformViewsController { int? lastClearedFocusViewId; - bool synchronizeToNativeViewHierarchy = true; - Map offsets = {}; void registerViewType(String viewType) { @@ -174,8 +172,6 @@ class FakeAndroidPlatformViewsController { return _clearFocus(call); case 'offset': return _offset(call); - case 'synchronizeToNativeViewHierarchy': - return _synchronizeToNativeViewHierarchy(call); } return Future.sync(() => null); } @@ -318,11 +314,6 @@ 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 064b0122536..01324add63f 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 { - expect( + expectLater( () { return PlatformViewsService.initAndroidView( id: 0, @@ -29,16 +29,25 @@ void main() { throwsA(isA()), ); - expect( - () { - return PlatformViewsService.initSurfaceAndroidView( - id: 0, - viewType: 'web', - layoutDirection: TextDirection.ltr, - ).create(); - }, - 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`'); + } }); test('create Android views', () async { @@ -47,13 +56,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), ]), ); }); @@ -65,7 +74,7 @@ void main() { viewType: 'webview', layoutDirection: TextDirection.ltr, ).setSize(const Size(100.0, 100.0)); - expect( + expectLater( () => PlatformViewsService.initAndroidView( id: 0, viewType: 'web', @@ -73,20 +82,6 @@ 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 { @@ -240,11 +235,6 @@ 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 befb278ad1b..f0c01ac7012 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -2312,43 +2312,6 @@ 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(