Unverified Commit 012952ba authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Don't build surface until the platform view has been created (#101399)

parent f5be639e
......@@ -176,7 +176,12 @@ class RenderAndroidView extends PlatformViewRenderBox {
Size targetSize;
do {
targetSize = size;
_currentTextureSize = await _viewController.setSize(targetSize);
if (_viewController.isCreated) {
_currentTextureSize = await _viewController.setSize(targetSize);
} else {
await _viewController.create(size: targetSize);
_currentTextureSize = targetSize;
}
// We've resized the platform view to targetSize, but it is possible that
// while we were resizing the render object's size was changed again.
// In that case we will resize the platform view again.
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
......@@ -750,16 +750,17 @@ abstract class AndroidViewController extends PlatformViewController {
return ((pointerId << 8) & 0xff00) | (action & 0xff);
}
/// Sends the message to dispose the platform view.
Future<void> _sendDisposeMessage();
Future<void> _sendCreateMessage();
/// Creates the Android View.
///
/// Throws an [AssertionError] if view was already disposed.
Future<void> create() async {
/// Sends the message to create the platform view with an initial [size].
Future<void> _sendCreateMessage({Size? size});
@override
Future<void> create({Size? size}) async {
assert(_state != _AndroidViewState.disposed, 'trying to create a disposed Android view');
await _sendCreateMessage();
await _sendCreateMessage(size: size);
_state = _AndroidViewState.created;
for (final PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) {
......@@ -970,7 +971,7 @@ class ExpensiveAndroidViewController extends AndroidViewController {
);
@override
Future<void> _sendCreateMessage() {
Future<void> _sendCreateMessage({Size? size}) {
final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': _viewType,
......@@ -996,8 +997,7 @@ class ExpensiveAndroidViewController extends AndroidViewController {
@override
Future<void> _sendDisposeMessage() {
return SystemChannels.platform_views
.invokeMethod<void>('dispose', <String, dynamic>{
return SystemChannels.platform_views.invokeMethod<void>('dispose', <String, dynamic>{
'id': viewId,
'hybrid': true,
});
......@@ -1014,11 +1014,12 @@ class ExpensiveAndroidViewController extends AndroidViewController {
}
}
/// Controls an Android view that is rendered to a texture.
/// Controls an Android view that is rendered as a texture.
/// This is typically used by [AndroidView] to display a View in the Android view hierarchy.
///
/// This is typically used by [AndroidView] to display an Android View in the Android view hierarchy.
/// The platform view is created by calling [create] with an initial size.
///
/// Typically created with [PlatformViewsService.initAndroidView].
/// The controller is typically created with [PlatformViewsService.initAndroidView].
class TextureAndroidViewController extends AndroidViewController {
TextureAndroidViewController._({
required int viewId,
......@@ -1044,25 +1045,16 @@ class TextureAndroidViewController extends AndroidViewController {
@override
int? get textureId => _textureId;
/// The size used to create the platform view.
Size? _initialSize;
/// The current offset of the platform view.
Offset _off = Offset.zero;
@override
Future<Size> setSize(Size size) async {
assert(_state != _AndroidViewState.disposed, 'trying to size a disposed Android View. View id: $viewId');
assert(_state != _AndroidViewState.disposed, 'Android view is disposed. View id: $viewId');
assert(_state != _AndroidViewState.waitingForSize, 'Android view must have an initial size. View id: $viewId');
assert(size != null);
assert(!size.isEmpty);
if (_state == _AndroidViewState.waitingForSize) {
_initialSize = size;
await create();
return size;
}
final Map<Object?, Object?>? meta = await SystemChannels.platform_views.invokeMapMethod<Object?, Object?>(
'resize',
<String, dynamic>{
......@@ -1077,6 +1069,15 @@ class TextureAndroidViewController extends AndroidViewController {
return Size(meta!['width']! as double, meta['height']! as double);
}
@override
Future<void> create({Size? size}) async {
if (size == null)
return;
assert(_state == _AndroidViewState.waitingForSize, 'Android view is already sized. View id: $viewId');
assert(!size.isEmpty);
return super.create(size: size);
}
@override
Future<void> setOffset(Offset off) async {
if (off == _off)
......@@ -1100,27 +1101,18 @@ class TextureAndroidViewController extends AndroidViewController {
);
}
/// Creates the Android View.
///
/// This should not be called before [AndroidViewController.setSize].
///
/// Throws an [AssertionError] if view was already disposed.
@override
Future<void> create() async {
if (_initialSize != null)
return super.create();
}
Future<void> _sendCreateMessage({Size? size}) async {
if (size == null)
return;
@override
Future<void> _sendCreateMessage() async {
assert(_initialSize != null, 'trying to create $TextureAndroidViewController without setting an initial size.');
assert(!_initialSize!.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.');
assert(!size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.');
final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': _viewType,
'width': _initialSize!.width,
'height': _initialSize!.height,
'width': size.width,
'height': size.height,
'direction': AndroidViewController._getAndroidDirection(_layoutDirection),
};
if (_creationParams != null) {
......@@ -1230,6 +1222,13 @@ abstract class PlatformViewController {
/// Dispatches the `event` to the platform view.
Future<void> dispatchPointerEvent(PointerEvent event);
/// Creates the platform view with the initial [size].
///
/// [size] is the view's initial size in logical pixel.
/// [size] can be omitted if the concrete implementation doesn't require an initial size
/// to create the platform view.
Future<void> create({Size? size}) async {}
/// Disposes the platform view.
///
/// The [PlatformViewController] is unusable after calling dispose.
......
......@@ -805,7 +805,6 @@ typedef CreatePlatformViewCallback = PlatformViewController Function(PlatformVie
/// The `surfaceFactory` and the `onCreatePlatformView` are only called when the
/// state of this widget is initialized, or when the `viewType` changes.
class PlatformViewLink extends StatefulWidget {
/// Construct a [PlatformViewLink] widget.
///
/// The `surfaceFactory` and the `onCreatePlatformView` must not be null.
......@@ -842,11 +841,19 @@ class PlatformViewLink extends StatefulWidget {
class _PlatformViewLinkState extends State<PlatformViewLink> {
int? _id;
PlatformViewController? _controller;
bool _platformViewCreated = false;
Widget? _surface;
FocusNode? _focusNode;
@override
Widget build(BuildContext context) {
if (_controller == null) {
return const SizedBox.expand();
}
if (!_platformViewCreated) {
// Depending on the platform, the initial size can be used to size the platform view.
return _PlatformViewPlaceHolder(onLayout: (Size size) => _controller!.create(size: size));
}
_surface ??= widget._surfaceFactory(context, _controller!);
return Focus(
focusNode: _focusNode,
......@@ -881,12 +888,18 @@ class _PlatformViewLinkState extends State<PlatformViewLink> {
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();
......@@ -1064,3 +1077,48 @@ class AndroidViewSurface extends PlatformViewSurface {
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);
/// A [RenderBox] that notifies its size to the owner after a layout.
class _PlatformViewPlaceholderBox extends RenderConstrainedBox {
_PlatformViewPlaceholderBox({
required this.onLayout,
}) : super(additionalConstraints: const BoxConstraints.tightFor(
width: double.infinity,
height: double.infinity,
));
_OnLayoutCallback onLayout;
@override
void performLayout() {
super.performLayout();
onLayout(size);
}
}
/// When a platform view is in the widget hierarchy, this widget is used to capture
/// the size of the platform view after the first layout.
/// This placeholder is basically a [SizedBox.expand] with a [onLayout] callback to
/// notify the size of the render object to its parent.
class _PlatformViewPlaceHolder extends SingleChildRenderObjectWidget {
const _PlatformViewPlaceHolder({
Key? key,
required this.onLayout,
}) : super(key: key);
final _OnLayoutCallback onLayout;
@override
_PlatformViewPlaceholderBox createRenderObject(BuildContext context) {
return _PlatformViewPlaceholderBox(onLayout: onLayout);
}
@override
void updateRenderObject(BuildContext context, _PlatformViewPlaceholderBox renderObject) {
renderObject.onLayout = onLayout;
}
}
......@@ -117,7 +117,7 @@ class FakeAndroidViewController implements AndroidViewController {
}
@override
Future<void> create() async {}
Future<void> create({Size? size}) async {}
@override
List<PlatformViewCreatedCallback> get createdCallbacks => <PlatformViewCreatedCallback>[];
......
......@@ -18,33 +18,32 @@ void main() {
});
test('create Android view of unregistered type', () async {
expectLater(
() {
return PlatformViewsService.initAndroidView(
id: 0,
viewType: 'web',
layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0));
},
await expectLater(() =>
PlatformViewsService.initAndroidView(
id: 0,
viewType: 'web',
layoutDirection: TextDirection.ltr,
).create(size: const Size(100.0, 100.0)),
throwsA(isA<PlatformException>()),
);
viewsController.registerViewType('web');
try {
await PlatformViewsService.initSurfaceAndroidView(
id: 0,
viewType: 'web',
layoutDirection: TextDirection.ltr,
).create();
).create(size: const Size(1.0, 1.0));
} catch (e) {
expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`');
}
try {
await PlatformViewsService.initAndroidView(
id: 0,
id: 1,
viewType: 'web',
layoutDirection: TextDirection.ltr,
).create();
).create(size: const Size(1.0, 1.0));
} catch (e) {
expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`');
}
......@@ -53,11 +52,9 @@ void main() {
test('create Android views', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr)
.setSize(const Size(100.0, 100.0));
.create(size: 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();
.create(size: const Size(200.0, 300.0));
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
......@@ -69,17 +66,21 @@ void main() {
test('reuse Android view id', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
final AndroidViewController controller = PlatformViewsService.initAndroidView(
id: 0,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0));
);
await controller.create(size: const Size(100.0, 100.0));
expectLater(
() => PlatformViewsService.initAndroidView(
id: 0,
viewType: 'web',
layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0)),
() {
final AndroidViewController controller = PlatformViewsService.initAndroidView(
id: 0,
viewType: 'web',
layoutDirection: TextDirection.ltr,
);
return controller.create(size: const Size(100.0, 100.0));
},
throwsA(isA<PlatformException>()),
);
});
......@@ -90,14 +91,22 @@ void main() {
id: 0,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0));
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
await viewController.setSize(const Size(200.0, 300.0));
).create(size: const Size(100.0, 100.0));
final AndroidViewController viewController = PlatformViewsService.initAndroidView(
id: 1,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
);
await viewController.create(size: const Size(200.0, 300.0));
await viewController.dispose();
final AndroidViewController surfaceViewController = PlatformViewsService.initSurfaceAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
await surfaceViewController.create();
final AndroidViewController surfaceViewController = PlatformViewsService.initSurfaceAndroidView(
id: 1,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
);
await surfaceViewController.create(size: const Size(200.0, 300.0));
await surfaceViewController.dispose();
expect(
......@@ -108,16 +117,14 @@ void main() {
);
});
test('dispose inexisting Android view', () async {
test('dispose Android view twice', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(
id: 0,
final AndroidViewController viewController = PlatformViewsService.initAndroidView(
id: 1,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0));
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
await viewController.setSize(const Size(200.0, 300.0));
);
await viewController.create(size: const Size(200.0, 300.0));
await viewController.dispose();
await viewController.dispose();
});
......@@ -131,7 +138,7 @@ void main() {
layoutDirection: TextDirection.ltr,
onFocus: () { didFocus = true; },
);
await viewController.setSize(const Size(100.0, 100.0));
await viewController.create(size: const Size(100.0, 100.0));
await viewController.dispose();
final ByteData message =
SystemChannels.platform_views.codec.encodeMethodCall(const MethodCall('viewFocused', 0));
......@@ -145,11 +152,16 @@ void main() {
id: 0,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
).setSize(const Size(100.0, 100.0));
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 1, viewType: 'webview', layoutDirection: TextDirection.ltr);
await viewController.setSize(const Size(200.0, 300.0));
await viewController.setSize(const Size(500.0, 500.0));
).create(size: const Size(100.0, 100.0));
final AndroidViewController androidView = PlatformViewsService.initAndroidView(
id: 1,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
);
await androidView.create(size: const Size(200.0, 300.0));
await androidView.setSize(const Size(500.0, 500.0));
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
......@@ -171,7 +183,7 @@ void main() {
)..addOnPlatformViewCreatedListener(callback);
expect(createdViews, isEmpty);
await controller1.setSize(const Size(100.0, 100.0));
await controller1.create(size: const Size(100.0, 100.0));
expect(createdViews, orderedEquals(<int>[0]));
final AndroidViewController controller2 = PlatformViewsService.initAndroidView(
......@@ -181,17 +193,20 @@ void main() {
)..addOnPlatformViewCreatedListener(callback);
expect(createdViews, orderedEquals(<int>[0]));
await controller2.setSize(const Size(100.0, 200.0));
await controller2.create(size: const Size(100.0, 200.0));
expect(createdViews, orderedEquals(<int>[0, 5]));
});
test("change Android view's directionality before creation", () async {
viewsController.registerViewType('webview');
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl);
final AndroidViewController viewController = PlatformViewsService.initAndroidView(
id: 0,
viewType: 'webview',
layoutDirection: TextDirection.rtl,
);
await viewController.setLayoutDirection(TextDirection.ltr);
await viewController.setSize(const Size(100.0, 100.0));
await viewController.create(size: const Size(100.0, 100.0));
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
......@@ -202,10 +217,13 @@ void main() {
test("change Android view's directionality after creation", () async {
viewsController.registerViewType('webview');
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
await viewController.setSize(const Size(100.0, 100.0));
final AndroidViewController viewController = PlatformViewsService.initAndroidView(
id: 0,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
);
await viewController.setLayoutDirection(TextDirection.rtl);
await viewController.create(size: const Size(100.0, 100.0));
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
......@@ -216,9 +234,12 @@ void main() {
test("set Android view's offset if view is created", () async {
viewsController.registerViewType('webview');
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 7, viewType: 'webview', layoutDirection: TextDirection.ltr);
await viewController.setSize(const Size(100.0, 100.0)); // Creates view.
final AndroidViewController viewController = PlatformViewsService.initAndroidView(
id: 7,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
);
await viewController.create(size: const Size(100.0, 100.0));
await viewController.setOffset(const Offset(10, 20));
expect(
viewsController.offsets,
......@@ -230,8 +251,11 @@ void main() {
test("doesn't set Android view's offset if view isn't created", () async {
viewsController.registerViewType('webview');
final AndroidViewController viewController =
PlatformViewsService.initAndroidView(id: 7, viewType: 'webview', layoutDirection: TextDirection.ltr);
final AndroidViewController viewController = PlatformViewsService.initAndroidView(
id: 7,
viewType: 'webview',
layoutDirection: TextDirection.ltr,
);
await viewController.setOffset(const Offset(10, 20));
expect(viewsController.offsets, equals(<int, Offset>{}));
});
......
......@@ -2312,6 +2312,50 @@ void main() {
expect(factoryInvocationCount, 1);
});
testWidgets(
'PlatformViewLink Widget init, should create a placeholder 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.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']),
);
onPlatformViewCreatedCallBack(createdPlatformViewId);
await tester.pump();
expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']),
);
expect(createdPlatformViewId, currentViewId + 1);
},
);
testWidgets('PlatformViewLink Widget dispose', (WidgetTester tester) async {
late FakePlatformViewController disposedController;
final PlatformViewLink platformViewLink = PlatformViewLink(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment