Reland: "Use texture layer when displaying an Android view" (#100990)

This commit is contained in:
Emmanuel Garcia 2022-03-29 19:40:17 -07:00 committed by GitHub
parent f07f6fef9a
commit 032205eaca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 342 additions and 263 deletions

View file

@ -2023,6 +2023,7 @@ targets:
- name: Linux_android hybrid_android_views_integration_test - name: Linux_android hybrid_android_views_integration_test
recipe: devicelab/devicelab_drone recipe: devicelab/devicelab_drone
presubmit: false presubmit: false
bringup: true # https://github.com/flutter/flutter/issues/100991
timeout: 60 timeout: 60
properties: properties:
tags: > tags: >

View file

@ -16,7 +16,8 @@ found in the LICENSE file. -->
android:launchMode="singleTop" android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
android:exported="true">
<meta-data <meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" /> android:value="true" />
@ -28,9 +29,5 @@ found in the LICENSE file. -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- Hybrid composition -->
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
</application> </application>
</manifest> </manifest>

View file

@ -17,6 +17,7 @@ class AndroidPlatformView extends StatelessWidget {
const AndroidPlatformView({ const AndroidPlatformView({
Key? key, Key? key,
this.onPlatformViewCreated, this.onPlatformViewCreated,
this.useHybridComposition = false,
required this.viewType, required this.viewType,
}) : assert(viewType != null), }) : assert(viewType != null),
super(key: key); super(key: key);
@ -31,6 +32,9 @@ class AndroidPlatformView extends StatelessWidget {
/// May be null. /// May be null.
final PlatformViewCreatedCallback? onPlatformViewCreated; final PlatformViewCreatedCallback? onPlatformViewCreated;
// Use hybrid composition.
final bool useHybridComposition;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PlatformViewLink( return PlatformViewLink(
@ -44,17 +48,27 @@ class AndroidPlatformView extends StatelessWidget {
); );
}, },
onCreatePlatformView: (PlatformViewCreationParams params) { onCreatePlatformView: (PlatformViewCreationParams params) {
final AndroidViewController controller = print('useHybridComposition=$useHybridComposition');
PlatformViewsService.initSurfaceAndroidView( late AndroidViewController controller;
if (useHybridComposition) {
controller = PlatformViewsService.initExpensiveAndroidView(
id: params.id, id: params.id,
viewType: params.viewType, viewType: params.viewType,
layoutDirection: TextDirection.ltr, layoutDirection: TextDirection.ltr,
) );
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated); } else {
controller = PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: params.viewType,
layoutDirection: TextDirection.ltr,
);
}
if (onPlatformViewCreated != null) { if (onPlatformViewCreated != null) {
controller.addOnPlatformViewCreatedListener(onPlatformViewCreated!); controller.addOnPlatformViewCreatedListener(onPlatformViewCreated!);
} }
return controller..create(); return controller
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
}, },
); );
} }

View file

@ -39,6 +39,7 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
int? id; int? id;
int nestedViewClickCount = 0; int nestedViewClickCount = 0;
bool showPlatformView = true; bool showPlatformView = true;
bool useHybridComposition = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -55,39 +56,63 @@ class NestedViewEventBodyState extends State<NestedViewEventBody> {
key: const ValueKey<String>('PlatformView'), key: const ValueKey<String>('PlatformView'),
viewType: 'simple_view', viewType: 'simple_view',
onPlatformViewCreated: onPlatformViewCreated, onPlatformViewCreated: onPlatformViewCreated,
useHybridComposition: useHybridComposition,
) : null, ) : null,
), ),
if (_lastTestStatus != _LastTestStatus.pending) _statusWidget(), if (_lastTestStatus != _LastTestStatus.pending) _statusWidget(),
if (viewChannel != null) ... <Widget>[ if (viewChannel != null) ... <Widget>[
ElevatedButton( Row(
key: const ValueKey<String>('ShowAlertDialog'), children: <Widget>[
onPressed: onShowAlertDialogPressed, Expanded(
child: const Text('SHOW ALERT DIALOG'), child: ElevatedButton(
), key: const ValueKey<String>('ShowAlertDialog'),
ElevatedButton( onPressed: onShowAlertDialogPressed,
key: const ValueKey<String>('TogglePlatformView'), child: const Text('SHOW ALERT DIALOG'),
onPressed: onTogglePlatformView, ),
child: const Text('TOGGLE PLATFORM VIEW'), ),
Expanded(
child: ElevatedButton(
key: const ValueKey<String>('TogglePlatformView'),
onPressed: onTogglePlatformView,
child: const Text('TOGGLE PLATFORM VIEW'),
),
),
],
), ),
Row( Row(
children: <Widget>[ children: <Widget>[
ElevatedButton( Expanded(
key: const ValueKey<String>('AddChildView'), child: ElevatedButton(
onPressed: onChildViewPressed, key: const ValueKey<String>('ToggleHybridComposition'),
child: const Text('ADD CHILD VIEW'), child: const Text('TOGGLE HC'),
), onPressed: () {
ElevatedButton( setState(() {
key: const ValueKey<String>('TapChildView'), useHybridComposition = !useHybridComposition;
onPressed: onTapChildViewPressed, });
child: const Text('TAP CHILD VIEW'), },
),
if (nestedViewClickCount > 0)
Text(
'Click count: $nestedViewClickCount',
key: const ValueKey<String>('NestedViewClickCount'),
), ),
),
Expanded(
child: ElevatedButton(
key: const ValueKey<String>('AddChildView'),
onPressed: onChildViewPressed,
child: const Text('ADD CHILD VIEW'),
),
),
Expanded(
child: ElevatedButton(
key: const ValueKey<String>('TapChildView'),
onPressed: onTapChildViewPressed,
child: const Text('TAP CHILD VIEW'),
),
),
], ],
), ),
if (nestedViewClickCount > 0)
Text(
'Click count: $nestedViewClickCount',
key: const ValueKey<String>('NestedViewClickCount'),
),
], ],
], ],
), ),

View file

@ -59,10 +59,58 @@ Future<void> main() async {
}, timeout: Timeout.none); }, timeout: Timeout.none);
}); });
group('Flutter surface switch', () { group('Flutter surface without hybrid composition', () {
setUpAll(() async { setUpAll(() async {
final SerializableFinder wmListTile = find.byValueKey('NestedViewEventTile'); await driver.tap(find.byValueKey('NestedViewEventTile'));
await driver.tap(wmListTile); });
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 { tearDownAll(() async {

View file

@ -52,7 +52,7 @@ Set<Type> _factoriesTypeSet<T>(Set<Factory<T>> factories) {
/// A render object for an Android view. /// A render object for an Android view.
/// ///
/// Requires Android API level 20 or greater. /// Requires Android API level 23 or greater.
/// ///
/// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an /// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an
/// Android [View](https://developer.android.com/reference/android/view/View). /// Android [View](https://developer.android.com/reference/android/view/View).
@ -74,7 +74,7 @@ Set<Type> _factoriesTypeSet<T>(Set<Factory<T>> factories) {
/// ///
/// * [AndroidView] which is a widget that is used to show an Android view. /// * [AndroidView] which is a widget that is used to show an Android view.
/// * [PlatformViewsService] which is a service for controlling platform views. /// * [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. /// Creates a render object for an Android view.
RenderAndroidView({ RenderAndroidView({
required AndroidViewController viewController, required AndroidViewController viewController,
@ -86,7 +86,8 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
assert(gestureRecognizers != null), assert(gestureRecognizers != null),
assert(clipBehavior != null), assert(clipBehavior != null),
_viewController = viewController, _viewController = viewController,
_clipBehavior = clipBehavior { _clipBehavior = clipBehavior,
super(controller: viewController, hitTestBehavior: hitTestBehavior, gestureRecognizers: gestureRecognizers) {
_viewController.pointTransformer = (Offset offset) => globalToLocal(offset); _viewController.pointTransformer = (Offset offset) => globalToLocal(offset);
updateGestureRecognizers(gestureRecognizers); updateGestureRecognizers(gestureRecognizers);
_viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
@ -101,18 +102,22 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
bool _isDisposed = false; bool _isDisposed = false;
/// The Android view controller for the Android view associated with this render object. /// The Android view controller for the Android view associated with this render object.
AndroidViewController get viewController => _viewController; @override
AndroidViewController get controller => _viewController;
AndroidViewController _viewController; AndroidViewController _viewController;
/// Sets a new Android view controller. /// Sets a new Android view controller.
/// @override
/// `viewController` must not be null. set controller(AndroidViewController controller) {
set viewController(AndroidViewController viewController) {
assert(_viewController != null); assert(_viewController != null);
assert(viewController != null); assert(controller != null);
if (_viewController == viewController) if (_viewController == controller)
return; return;
_viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated); _viewController.removeOnPlatformViewCreatedListener(_onPlatformViewCreated);
_viewController = viewController; super.controller = controller;
_viewController = controller;
_viewController.pointTransformer = (Offset offset) => globalToLocal(offset);
_sizePlatformView(); _sizePlatformView();
if (_viewController.isCreated) { if (_viewController.isCreated) {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
@ -138,26 +143,6 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
markNeedsSemanticsUpdate(); 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<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
_updateGestureRecognizersWithCallBack(gestureRecognizers, _viewController.dispatchPointerEvent);
}
@override @override
bool get sizedByParent => true; bool get sizedByParent => true;
@ -182,9 +167,8 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
// Android virtual displays cannot have a zero size. // Android virtual displays cannot have a zero size.
// Trying to size it to 0 crashes the app, which was happening when starting the app // 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). // 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; return;
}
_state = _PlatformViewState.resizing; _state = _PlatformViewState.resizing;
markNeedsPaint(); markNeedsPaint();
@ -212,7 +196,8 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
void _setOffset() { void _setOffset() {
SchedulerBinding.instance.addPostFrameCallback((_) async { SchedulerBinding.instance.addPostFrameCallback((_) async {
if (!_isDisposed) { if (!_isDisposed) {
await _viewController.setOffset(localToGlobal(Offset.zero)); if (attached)
await _viewController.setOffset(localToGlobal(Offset.zero));
// Schedule a new post frame callback. // Schedule a new post frame callback.
_setOffset(); _setOffset();
} }
@ -221,7 +206,7 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_viewController.textureId == null) if (_viewController.textureId == null || _currentTextureSize == null)
return; return;
// As resizing the Android view happens asynchronously we don't know exactly when is a // 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( context.addLayer(TextureLayer(
rect: offset & _currentTextureSize!, rect: offset & _currentTextureSize!,
textureId: viewController.textureId!, textureId: _viewController.textureId!,
)); ));
} }
@override @override
void describeSemanticsConfiguration (SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(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; config.isSemanticBoundary = true;
if (_viewController.isCreated) { if (_viewController.isCreated) {
@ -339,7 +325,7 @@ class RenderUiKitView extends RenderBox {
// any newly arriving events there's nothing we need to invalidate. // any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior; PlatformViewHitTestBehavior hitTestBehavior;
/// {@macro flutter.rendering.RenderAndroidView.updateGestureRecognizers} /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) { void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(gestureRecognizers != null); assert(gestureRecognizers != null);
assert( assert(
@ -653,11 +639,11 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
PlatformViewController get controller => _controller; PlatformViewController get controller => _controller;
PlatformViewController _controller; PlatformViewController _controller;
/// This value must not be null, and setting it to a new value will result in a repaint. /// 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 != null);
assert(controller.viewId != null && controller.viewId > -1); assert(controller.viewId != null && controller.viewId > -1);
if ( _controller == controller) { if (_controller == controller) {
return; return;
} }
final bool needsSemanticsUpdate = _controller.viewId != controller.viewId; 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 /// Any active gesture arena the `PlatformView` participates in is rejected when the
/// set of gesture recognizers is changed. /// set of gesture recognizers is changed.
@ -700,7 +698,7 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
} }
@override @override
void describeSemanticsConfiguration (SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
assert(_controller.viewId != null); assert(_controller.viewId != null);
config.isSemanticBoundary = true; config.isSemanticBoundary = true;

View file

@ -76,10 +76,8 @@ class PlatformViewsService {
/// The callbacks are invoked when the platform view asks to be focused. /// The callbacks are invoked when the platform view asks to be focused.
final Map<int, VoidCallback> _focusCallbacks = <int, VoidCallback>{}; final Map<int, VoidCallback> _focusCallbacks = <int, VoidCallback>{};
/// {@template flutter.services.PlatformViewsService.initAndroidView}
/// Creates a [TextureAndroidViewController] for a new Android view. /// Creates a controller for a new Android view.
///
/// The view is created after calling [TextureAndroidViewController.setSize].
/// ///
/// `id` is an unused unique identifier generated with [platformViewsRegistry]. /// `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. /// The `id, `viewType, and `layoutDirection` parameters must not be null.
/// If `creationParams` is non null then `creationParamsCodec` 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 int id,
required String viewType, required String viewType,
required TextDirection layoutDirection, required TextDirection layoutDirection,
@ -128,32 +127,11 @@ class PlatformViewsService {
return controller; return controller;
} }
/// Creates a [SurfaceAndroidViewController] for a new Android view. /// {@macro flutter.services.PlatformViewsService.initAndroidView}
/// ///
/// The view is created after calling [AndroidViewController.create]. /// Alias for [initAndroidView].
/// /// This factory is provided for backward compatibility purposes.
/// `id` is an unused unique identifier generated with [platformViewsRegistry]. /// In the future, this method will be deprecated.
///
/// `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.
static SurfaceAndroidViewController initSurfaceAndroidView({ static SurfaceAndroidViewController initSurfaceAndroidView({
required int id, required int id,
required String viewType, required String viewType,
@ -174,28 +152,43 @@ class PlatformViewsService {
creationParams: creationParams, creationParams: creationParams,
creationParamsCodec: creationParamsCodec, 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<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) {
final ExpensiveAndroidViewController controller = ExpensiveAndroidViewController._(
viewId: id,
viewType: viewType,
layoutDirection: layoutDirection,
creationParams: creationParams,
creationParamsCodec: creationParamsCodec,
);
_instance._focusCallbacks[id] = onFocus ?? () {}; _instance._focusCallbacks[id] = onFocus ?? () {};
return controller; return controller;
} }
/// Whether the render surface of the Android `FlutterView` should be converted to a `FlutterImageView`. /// Whether the render surface of the Android `FlutterView` should be converted to a `FlutterImageView`.
/// @Deprecated(
/// When adding platform views using 'No longer necessary to improve performance. '
/// [Hybrid Composition](https://flutter.dev/docs/development/platform-integration/platform-views), 'This feature was deprecated after v2.11.0-0.1.pre.',
/// the engine converts the render surface to a `FlutterImageView` to improve )
/// animation synchronization between Flutter widgets and the Android platform static Future<void> synchronizeToNativeViewHierarchy(bool yes) async {}
/// views. On Android versions < 10, this can have some performance issues.
/// This flag allows disabling this conversion.
///
/// Defaults to true.
static Future<void> synchronizeToNativeViewHierarchy(bool yes) {
assert(defaultTargetPlatform == TargetPlatform.android);
return SystemChannels.platform_views.invokeMethod<void>(
'synchronizeToNativeViewHierarchy',
yes,
);
}
// TODO(amirh): reference the iOS plugin API for registering a UIView factory once it lands. // 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. /// 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; 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]. /// Typically created with [PlatformViewsService.initAndroidView].
// TODO(bparrishMines): Remove abstract methods that are not required by all subclasses. // TODO(bparrishMines): Remove abstract methods that are not required by all subclasses.
@ -676,7 +669,6 @@ abstract class AndroidViewController extends PlatformViewController {
required TextDirection layoutDirection, required TextDirection layoutDirection,
dynamic creationParams, dynamic creationParams,
MessageCodec<dynamic>? creationParamsCodec, MessageCodec<dynamic>? creationParamsCodec,
bool waitingForSize = false,
}) : assert(viewId != null), }) : assert(viewId != null),
assert(viewType != null), assert(viewType != null),
assert(layoutDirection != null), assert(layoutDirection != null),
@ -684,10 +676,7 @@ abstract class AndroidViewController extends PlatformViewController {
_viewType = viewType, _viewType = viewType,
_layoutDirection = layoutDirection, _layoutDirection = layoutDirection,
_creationParams = creationParams, _creationParams = creationParams,
_creationParamsCodec = creationParamsCodec, _creationParamsCodec = creationParamsCodec;
_state = waitingForSize
? _AndroidViewState.waitingForSize
: _AndroidViewState.creating;
/// Action code for when a primary pointer touched the screen. /// Action code for when a primary pointer touched the screen.
/// ///
@ -737,7 +726,7 @@ abstract class AndroidViewController extends PlatformViewController {
TextDirection _layoutDirection; TextDirection _layoutDirection;
_AndroidViewState _state; _AndroidViewState _state = _AndroidViewState.waitingForSize;
final dynamic _creationParams; final dynamic _creationParams;
@ -848,10 +837,16 @@ abstract class AndroidViewController extends PlatformViewController {
/// Removes a callback added with [addOnPlatformViewCreatedListener]. /// Removes a callback added with [addOnPlatformViewCreatedListener].
void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
assert(listener != null);
assert(_state != _AndroidViewState.disposed); assert(_state != _AndroidViewState.disposed);
_platformViewCreatedCallbacks.remove(listener); _platformViewCreatedCallbacks.remove(listener);
} }
/// The created callbacks that are invoked after the platform view has been
/// created.
@visibleForTesting
List<PlatformViewCreatedCallback> get createdCallbacks => _platformViewCreatedCallbacks;
/// Sets the layout direction for the Android view. /// Sets the layout direction for the Android view.
Future<void> setLayoutDirection(TextDirection layoutDirection) async { Future<void> setLayoutDirection(TextDirection layoutDirection) async {
assert( assert(
@ -938,11 +933,29 @@ abstract class AndroidViewController extends PlatformViewController {
} }
} }
/// Controls an Android view by rendering to an [AndroidViewSurface]. /// Controls an Android view that is composed using a GL texture.
/// /// This controller is created from the [PlatformViewsService.initSurfaceAndroidView] factory,
/// Typically created with [PlatformViewsService.initAndroidView]. /// and is defined for backward compatibility.
class SurfaceAndroidViewController extends AndroidViewController { class SurfaceAndroidViewController extends TextureAndroidViewController{
SurfaceAndroidViewController._({ SurfaceAndroidViewController._({
required int viewId,
required String viewType,
required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic>? 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 int viewId,
required String viewType, required String viewType,
required TextDirection layoutDirection, required TextDirection layoutDirection,
@ -1019,7 +1032,6 @@ class TextureAndroidViewController extends AndroidViewController {
layoutDirection: layoutDirection, layoutDirection: layoutDirection,
creationParams: creationParams, creationParams: creationParams,
creationParamsCodec: creationParamsCodec, creationParamsCodec: creationParamsCodec,
waitingForSize: true,
); );
/// The texture entry id into which the Android view is rendered. /// The texture entry id into which the Android view is rendered.
@ -1032,7 +1044,8 @@ class TextureAndroidViewController extends AndroidViewController {
@override @override
int? get textureId => _textureId; int? get textureId => _textureId;
late Size _initialSize; /// The size used to create the platform view.
Size? _initialSize;
/// The current offset of the platform view. /// The current offset of the platform view.
Offset _off = Offset.zero; Offset _off = Offset.zero;
@ -1047,7 +1060,7 @@ class TextureAndroidViewController extends AndroidViewController {
if (_state == _AndroidViewState.waitingForSize) { if (_state == _AndroidViewState.waitingForSize) {
_initialSize = size; _initialSize = size;
await create(); await create();
return _initialSize; return size;
} }
final Map<Object?, Object?>? meta = await SystemChannels.platform_views.invokeMapMethod<Object?, Object?>( final Map<Object?, Object?>? meta = await SystemChannels.platform_views.invokeMapMethod<Object?, Object?>(
@ -1093,17 +1106,21 @@ class TextureAndroidViewController extends AndroidViewController {
/// ///
/// Throws an [AssertionError] if view was already disposed. /// Throws an [AssertionError] if view was already disposed.
@override @override
Future<void> create() => super.create(); Future<void> create() async {
if (_initialSize != null)
return super.create();
}
@override @override
Future<void> _sendCreateMessage() async { Future<void> _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<String, dynamic> args = <String, dynamic>{ final Map<String, dynamic> args = <String, dynamic>{
'id': viewId, 'id': viewId,
'viewType': _viewType, 'viewType': _viewType,
'width': _initialSize.width, 'width': _initialSize!.width,
'height': _initialSize.height, 'height': _initialSize!.height,
'direction': AndroidViewController._getAndroidDirection(_layoutDirection), 'direction': AndroidViewController._getAndroidDirection(_layoutDirection),
}; };
if (_creationParams != null) { if (_creationParams != null) {

View file

@ -15,7 +15,7 @@ import 'framework.dart';
/// Embeds an Android view in the Widget hierarchy. /// 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 /// Embedding Android views is an expensive operation and should be avoided when a Flutter
/// equivalent is possible. /// equivalent is possible.
@ -681,7 +681,7 @@ class _AndroidPlatformView extends LeafRenderObjectWidget {
@override @override
void updateRenderObject(BuildContext context, RenderAndroidView renderObject) { void updateRenderObject(BuildContext context, RenderAndroidView renderObject) {
renderObject.viewController = controller; renderObject.controller = controller;
renderObject.hitTestBehavior = hitTestBehavior; renderObject.hitTestBehavior = hitTestBehavior;
renderObject.updateGestureRecognizers(gestureRecognizers); renderObject.updateGestureRecognizers(gestureRecognizers);
renderObject.clipBehavior = clipBehavior; renderObject.clipBehavior = clipBehavior;
@ -842,15 +842,11 @@ class PlatformViewLink extends StatefulWidget {
class _PlatformViewLinkState extends State<PlatformViewLink> { class _PlatformViewLinkState extends State<PlatformViewLink> {
int? _id; int? _id;
PlatformViewController? _controller; PlatformViewController? _controller;
bool _platformViewCreated = false;
Widget? _surface; Widget? _surface;
FocusNode? _focusNode; FocusNode? _focusNode;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_platformViewCreated) {
return const SizedBox.expand();
}
_surface ??= widget._surfaceFactory(context, _controller!); _surface ??= widget._surfaceFactory(context, _controller!);
return Focus( return Focus(
focusNode: _focusNode, focusNode: _focusNode,
@ -875,9 +871,6 @@ class _PlatformViewLinkState extends State<PlatformViewLink> {
// The _surface has to be recreated as its controller is disposed. // The _surface has to be recreated as its controller is disposed.
// Setting _surface to null will trigger its creation in build(). // Setting _surface to null will trigger its creation in build().
_surface = null; _surface = null;
// We are about to create a new platform view.
_platformViewCreated = false;
_initialize(); _initialize();
} }
} }
@ -888,16 +881,12 @@ class _PlatformViewLinkState extends State<PlatformViewLink> {
PlatformViewCreationParams._( PlatformViewCreationParams._(
id: _id!, id: _id!,
viewType: widget.viewType, viewType: widget.viewType,
onPlatformViewCreated: _onPlatformViewCreated, onPlatformViewCreated: (_) {},
onFocusChanged: _handlePlatformFocusChanged, onFocusChanged: _handlePlatformFocusChanged,
), ),
); );
} }
void _onPlatformViewCreated(int id) {
setState(() { _platformViewCreated = true; });
}
void _handleFrameworkFocusChanged(bool isFocused) { void _handleFrameworkFocusChanged(bool isFocused) {
if (!isFocused) { if (!isFocused) {
_controller?.clearFocus(); _controller?.clearFocus();
@ -1020,18 +1009,18 @@ class PlatformViewSurface extends LeafRenderObjectWidget {
/// Integrates an Android view with Flutter's compositor, touch, and semantics subsystems. /// 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] /// The compositor integration is done by adding a [TextureLayer] to the layer tree.
/// isn't supported on all platforms. Custom Flutter embedders can support
/// [PlatformViewLayer]s by implementing a SystemCompositor.
/// ///
/// The widget fills all available space, the parent of this object must provide bounded layout /// The parent of this object must provide bounded layout constraints.
/// constraints.
/// ///
/// If the associated platform view is not created, the [AndroidViewSurface] does not paint any contents. /// 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: /// 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. /// * [UiKitView] which embeds an iOS platform view in the widget hierarchy.
class AndroidViewSurface extends PlatformViewSurface { class AndroidViewSurface extends PlatformViewSurface {
/// Construct an `AndroidPlatformViewSurface`. /// Construct an `AndroidPlatformViewSurface`.
@ -1052,12 +1041,26 @@ class AndroidViewSurface extends PlatformViewSurface {
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
final PlatformViewRenderBox renderBox = final AndroidViewController viewController = controller as AndroidViewController;
super.createRenderObject(context) as PlatformViewRenderBox; // Compose using the Android view hierarchy.
// This is useful when embedding a SurfaceView into a Flutter app.
(controller as AndroidViewController).pointTransformer = // 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); (Offset position) => renderBox.globalToLocal(position);
return renderBox; return renderBox;
} }
} }

View file

@ -151,6 +151,38 @@ void main() {
// Passes if no crashes. // 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: <Factory<OneSequenceGestureRecognizer>>{},
);
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', () { test('render object changed its visual appearance after texture is created', () {
FakeAsync().run((FakeAsync async) { FakeAsync().run((FakeAsync async) {
final AndroidViewController viewController = final AndroidViewController viewController =

View file

@ -84,24 +84,23 @@ class FakeAndroidViewController implements AndroidViewController {
@override @override
Future<Size> setSize(Size size) { Future<Size> setSize(Size size) {
throw UnimplementedError(); return Future<Size>.value(size);
} }
@override @override
Future<void> setOffset(Offset off) { Future<void> setOffset(Offset off) async {}
throw UnimplementedError();
@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 @override
void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
throw UnimplementedError(); throw UnimplementedError();
@ -118,9 +117,10 @@ class FakeAndroidViewController implements AndroidViewController {
} }
@override @override
Future<void> create() async { Future<void> create() async {}
created = true;
} @override
List<PlatformViewCreatedCallback> get createdCallbacks => <PlatformViewCreatedCallback>[];
} }
class FakeAndroidPlatformViewsController { class FakeAndroidPlatformViewsController {
@ -143,8 +143,6 @@ class FakeAndroidPlatformViewsController {
int? lastClearedFocusViewId; int? lastClearedFocusViewId;
bool synchronizeToNativeViewHierarchy = true;
Map<int, Offset> offsets = <int, Offset>{}; Map<int, Offset> offsets = <int, Offset>{};
void registerViewType(String viewType) { void registerViewType(String viewType) {
@ -174,8 +172,6 @@ class FakeAndroidPlatformViewsController {
return _clearFocus(call); return _clearFocus(call);
case 'offset': case 'offset':
return _offset(call); return _offset(call);
case 'synchronizeToNativeViewHierarchy':
return _synchronizeToNativeViewHierarchy(call);
} }
return Future<dynamic>.sync(() => null); return Future<dynamic>.sync(() => null);
} }
@ -318,11 +314,6 @@ class FakeAndroidPlatformViewsController {
lastClearedFocusViewId = id; lastClearedFocusViewId = id;
return Future<dynamic>.sync(() => null); return Future<dynamic>.sync(() => null);
} }
Future<dynamic> _synchronizeToNativeViewHierarchy(MethodCall call) {
synchronizeToNativeViewHierarchy = call.arguments as bool;
return Future<dynamic>.sync(() => null);
}
} }
class FakeIosPlatformViewsController { class FakeIosPlatformViewsController {

View file

@ -18,7 +18,7 @@ void main() {
}); });
test('create Android view of unregistered type', () async { test('create Android view of unregistered type', () async {
expect( expectLater(
() { () {
return PlatformViewsService.initAndroidView( return PlatformViewsService.initAndroidView(
id: 0, id: 0,
@ -29,16 +29,25 @@ void main() {
throwsA(isA<PlatformException>()), throwsA(isA<PlatformException>()),
); );
expect( try {
() { await PlatformViewsService.initSurfaceAndroidView(
return PlatformViewsService.initSurfaceAndroidView( id: 0,
id: 0, viewType: 'web',
viewType: 'web', layoutDirection: TextDirection.ltr,
layoutDirection: TextDirection.ltr, ).create();
).create(); } catch (e) {
}, expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`');
throwsA(isA<PlatformException>()), }
);
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 { test('create Android views', () async {
@ -47,13 +56,13 @@ void main() {
.setSize(const Size(100.0, 100.0)); .setSize(const Size(100.0, 100.0));
await PlatformViewsService.initAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl) await PlatformViewsService.initAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl)
.setSize(const Size(200.0, 300.0)); .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(); await PlatformViewsService.initSurfaceAndroidView(id: 2, viewType: 'webview', layoutDirection: TextDirection.rtl).create();
expect( expect(
viewsController.views, viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[ unorderedEquals(<FakeAndroidPlatformView>[
const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, null), 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(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', viewType: 'webview',
layoutDirection: TextDirection.ltr, layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0)); ).setSize(const Size(100.0, 100.0));
expect( expectLater(
() => PlatformViewsService.initAndroidView( () => PlatformViewsService.initAndroidView(
id: 0, id: 0,
viewType: 'web', viewType: 'web',
@ -73,20 +82,6 @@ void main() {
).setSize(const Size(100.0, 100.0)), ).setSize(const Size(100.0, 100.0)),
throwsA(isA<PlatformException>()), throwsA(isA<PlatformException>()),
); );
await PlatformViewsService.initSurfaceAndroidView(
id: 1,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
).create();
expect(
() => PlatformViewsService.initSurfaceAndroidView(
id: 1,
viewType: 'web',
layoutDirection: TextDirection.ltr,
).create(),
throwsA(isA<PlatformException>()),
);
}); });
test('dispose Android view', () async { test('dispose Android view', () async {
@ -240,11 +235,6 @@ void main() {
await viewController.setOffset(const Offset(10, 20)); await viewController.setOffset(const Offset(10, 20));
expect(viewsController.offsets, equals(<int, Offset>{})); expect(viewsController.offsets, equals(<int, Offset>{}));
}); });
test('synchronizeToNativeViewHierarchy', () async {
await PlatformViewsService.synchronizeToNativeViewHierarchy(false);
expect(viewsController.synchronizeToNativeViewHierarchy, false);
});
}); });
group('iOS', () { group('iOS', () {

View file

@ -2312,43 +2312,6 @@ void main() {
expect(factoryInvocationCount, 1); 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 <Factory<OneSequenceGestureRecognizer>>{},
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);
await tester.pumpWidget(platformViewLink);
expect(() => tester.allWidgets.whereType<SizedBox>().first, returnsNormally);
onPlatformViewCreatedCallBack(createdPlatformViewId);
await tester.pump();
expect(() => tester.allWidgets.whereType<PlatformViewSurface>().first, returnsNormally);
expect(createdPlatformViewId, currentViewId + 1);
},
);
testWidgets('PlatformViewLink Widget dispose', (WidgetTester tester) async { testWidgets('PlatformViewLink Widget dispose', (WidgetTester tester) async {
late FakePlatformViewController disposedController; late FakePlatformViewController disposedController;
final PlatformViewLink platformViewLink = PlatformViewLink( final PlatformViewLink platformViewLink = PlatformViewLink(