Unverified Commit f58e8f56 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Fix AndroidView offset and resize (#99888)

parent bb4a5fa7
...@@ -6,6 +6,7 @@ import 'dart:ui'; ...@@ -6,6 +6,7 @@ import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -90,10 +91,15 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { ...@@ -90,10 +91,15 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
updateGestureRecognizers(gestureRecognizers); updateGestureRecognizers(gestureRecognizers);
_viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
this.hitTestBehavior = hitTestBehavior; this.hitTestBehavior = hitTestBehavior;
_setOffset();
} }
_PlatformViewState _state = _PlatformViewState.uninitialized; _PlatformViewState _state = _PlatformViewState.uninitialized;
Size? _currentTextureSize;
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; AndroidViewController get viewController => _viewController;
AndroidViewController _viewController; AndroidViewController _viewController;
...@@ -172,8 +178,6 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { ...@@ -172,8 +178,6 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
_sizePlatformView(); _sizePlatformView();
} }
late Size _currentAndroidViewSize;
Future<void> _sizePlatformView() async { Future<void> _sizePlatformView() async {
// 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
...@@ -188,8 +192,7 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { ...@@ -188,8 +192,7 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
Size targetSize; Size targetSize;
do { do {
targetSize = size; targetSize = size;
await _viewController.setSize(targetSize); _currentTextureSize = await _viewController.setSize(targetSize);
_currentAndroidViewSize = targetSize;
// We've resized the platform view to targetSize, but it is possible that // 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. // while we were resizing the render object's size was changed again.
// In that case we will resize the platform view again. // In that case we will resize the platform view again.
...@@ -199,14 +202,39 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { ...@@ -199,14 +202,39 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
markNeedsPaint(); markNeedsPaint();
} }
// Sets the offset of the underlaying platform view on the platform side.
//
// This allows the Android native view to draw the a11y highlights in the same
// location on the screen as the platform view widget in the Flutter framework.
//
// It also allows platform code to obtain the correct position of the Android
// native view on the screen.
void _setOffset() {
SchedulerBinding.instance.addPostFrameCallback((_) async {
if (!_isDisposed) {
await _viewController.setOffset(localToGlobal(Offset.zero));
// Schedule a new post frame callback.
_setOffset();
}
});
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_viewController.textureId == null) if (_viewController.textureId == null)
return; return;
// Clip the texture if it's going to paint out of the bounds of the renter box // As resizing the Android view happens asynchronously we don't know exactly when is a
// (see comment in _paintTexture for an explanation of when this happens). // texture frame with the new size is ready for consumption.
if ((size.width < _currentAndroidViewSize.width || size.height < _currentAndroidViewSize.height) && clipBehavior != Clip.none) { // TextureLayer is unaware of the texture frame's size and always maps it to the
// specified rect. If the rect we provide has a different size from the current texture frame's
// size the texture frame will be scaled.
// To prevent unwanted scaling artifacts while resizing, clip the texture.
// This guarantees that the size of the texture frame we're painting is always
// _currentAndroidTextureSize.
final bool isTextureLargerThanWidget = _currentTextureSize!.width > size.width ||
_currentTextureSize!.height > size.height;
if (isTextureLargerThanWidget && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect( _clipRectLayer.layer = context.pushClipRect(
true, true,
offset, offset,
...@@ -225,24 +253,18 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin { ...@@ -225,24 +253,18 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
@override @override
void dispose() { void dispose() {
_isDisposed = true;
_clipRectLayer.layer = null; _clipRectLayer.layer = null;
super.dispose(); super.dispose();
} }
void _paintTexture(PaintingContext context, Offset offset) { void _paintTexture(PaintingContext context, Offset offset) {
// As resizing the Android view happens asynchronously we don't know exactly when is a if (_currentTextureSize == null)
// texture frame with the new size is ready for consumption. return;
// TextureLayer is unaware of the texture frame's size and always maps it to the
// specified rect. If the rect we provide has a different size from the current texture frame's
// size the texture frame will be scaled.
// To prevent unwanted scaling artifacts while resizing we freeze the texture frame, until
// we know that a frame with the new size is in the buffer.
// This guarantees that the size of the texture frame we're painting is always
// _currentAndroidViewSize.
context.addLayer(TextureLayer( context.addLayer(TextureLayer(
rect: offset & _currentAndroidViewSize, rect: offset & _currentTextureSize!,
textureId: _viewController.textureId!, textureId: viewController.textureId!,
freeze: _state == _PlatformViewState.resizing,
)); ));
} }
......
...@@ -780,11 +780,27 @@ abstract class AndroidViewController extends PlatformViewController { ...@@ -780,11 +780,27 @@ abstract class AndroidViewController extends PlatformViewController {
/// Sizes the Android View. /// Sizes the Android View.
/// ///
/// `size` is the view's new size in logical pixel, it must not be null and must /// [size] is the view's new size in logical pixel, it must not be null and must
/// be bigger than zero. /// be bigger than zero.
/// ///
/// The first time a size is set triggers the creation of the Android view. /// The first time a size is set triggers the creation of the Android view.
Future<void> setSize(Size size); ///
/// Returns the buffer size in logical pixel that backs the texture where the platform
/// view pixels are written to.
///
/// The buffer size may or may not be the same as [size].
///
/// As a result, consumers are expected to clip the texture using [size], while using
/// the return value to size the texture.
Future<Size> setSize(Size size);
/// Sets the offset of the platform view.
///
/// [off] is the view's new offset in logical pixel.
///
/// On Android, this allows the Android native view to draw the a11y highlights in the same
/// location on the screen as the platform view widget in the Flutter framework.
Future<void> setOffset(Offset off);
/// Returns the texture entry id that the Android view is rendering into. /// Returns the texture entry id that the Android view is rendering into.
/// ///
...@@ -975,15 +991,19 @@ class SurfaceAndroidViewController extends AndroidViewController { ...@@ -975,15 +991,19 @@ class SurfaceAndroidViewController extends AndroidViewController {
} }
@override @override
Future<void> setSize(Size size) { Future<Size> setSize(Size size) {
throw UnimplementedError('Not supported for $SurfaceAndroidViewController.');
}
@override
Future<void> setOffset(Offset off) {
throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); throw UnimplementedError('Not supported for $SurfaceAndroidViewController.');
} }
} }
/// Controls an Android view that is rendered to a texture. /// Controls an Android view that is rendered to a texture.
/// ///
/// This is typically used by [AndroidView] to display an Android View in a /// This is typically used by [AndroidView] to display an Android View in the Android view hierarchy.
/// [VirtualDisplay](https://developer.android.com/reference/android/hardware/display/VirtualDisplay).
/// ///
/// Typically created with [PlatformViewsService.initAndroidView]. /// Typically created with [PlatformViewsService.initAndroidView].
class TextureAndroidViewController extends AndroidViewController { class TextureAndroidViewController extends AndroidViewController {
...@@ -1012,25 +1032,59 @@ class TextureAndroidViewController extends AndroidViewController { ...@@ -1012,25 +1032,59 @@ class TextureAndroidViewController extends AndroidViewController {
@override @override
int? get textureId => _textureId; int? get textureId => _textureId;
late Size _size; late Size _initialSize;
/// The current offset of the platform view.
Offset _off = Offset.zero;
@override @override
Future<void> setSize(Size size) async { Future<Size> setSize(Size size) async {
assert(_state != _AndroidViewState.disposed, 'trying to size a disposed Android View. View id: $viewId'); assert(_state != _AndroidViewState.disposed, 'trying to size a disposed Android View. View id: $viewId');
assert(size != null); assert(size != null);
assert(!size.isEmpty); assert(!size.isEmpty);
if (_state == _AndroidViewState.waitingForSize) { if (_state == _AndroidViewState.waitingForSize) {
_size = size; _initialSize = size;
return create(); await create();
return _initialSize;
} }
await SystemChannels.platform_views.invokeMethod<void>('resize', <String, dynamic>{ final Map<Object?, Object?>? meta = await SystemChannels.platform_views.invokeMapMethod<Object?, Object?>(
'resize',
<String, dynamic>{
'id': viewId, 'id': viewId,
'width': size.width, 'width': size.width,
'height': size.height, 'height': size.height,
}); },
);
assert(meta != null);
assert(meta!.containsKey('width'));
assert(meta!.containsKey('height'));
return Size(meta!['width']! as double, meta['height']! as double);
}
@override
Future<void> setOffset(Offset off) async {
if (off == _off)
return;
// Don't set the offset unless the Android view has been created.
// The implementation of this method channel throws if the Android view for this viewId
// isn't addressable.
if (_state != _AndroidViewState.created)
return;
_off = off;
await SystemChannels.platform_views.invokeMethod<void>(
'offset',
<String, dynamic>{
'id': viewId,
'top': off.dy,
'left': off.dx,
},
);
} }
/// Creates the Android View. /// Creates the Android View.
...@@ -1043,13 +1097,13 @@ class TextureAndroidViewController extends AndroidViewController { ...@@ -1043,13 +1097,13 @@ class TextureAndroidViewController extends AndroidViewController {
@override @override
Future<void> _sendCreateMessage() async { Future<void> _sendCreateMessage() async {
assert(!_size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid 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': _size.width, 'width': _initialSize.width,
'height': _size.height, 'height': _initialSize.height,
'direction': AndroidViewController._getAndroidDirection(_layoutDirection), 'direction': AndroidViewController._getAndroidDirection(_layoutDirection),
}; };
if (_creationParams != null) { if (_creationParams != null) {
......
...@@ -546,7 +546,7 @@ class _AndroidViewState extends State<AndroidView> { ...@@ -546,7 +546,7 @@ class _AndroidViewState extends State<AndroidView> {
} }
SystemChannels.textInput.invokeMethod<void>( SystemChannels.textInput.invokeMethod<void>(
'TextInput.setPlatformViewClient', 'TextInput.setPlatformViewClient',
<String, dynamic>{'platformViewId': _id, 'usesVirtualDisplay': true}, <String, dynamic>{'platformViewId': _id},
).catchError((dynamic e) { ).catchError((dynamic e) {
if (e is MissingPluginException) { if (e is MissingPluginException) {
// We land the framework part of Android platform views keyboard // We land the framework part of Android platform views keyboard
......
...@@ -83,7 +83,12 @@ class FakeAndroidViewController implements AndroidViewController { ...@@ -83,7 +83,12 @@ class FakeAndroidViewController implements AndroidViewController {
} }
@override @override
Future<void> setSize(Size size) { Future<Size> setSize(Size size) {
throw UnimplementedError();
}
@override
Future<void> setOffset(Offset off) {
throw UnimplementedError(); throw UnimplementedError();
} }
...@@ -140,6 +145,8 @@ class FakeAndroidPlatformViewsController { ...@@ -140,6 +145,8 @@ class FakeAndroidPlatformViewsController {
bool synchronizeToNativeViewHierarchy = true; bool synchronizeToNativeViewHierarchy = true;
Map<int, Offset> offsets = <int, Offset>{};
void registerViewType(String viewType) { void registerViewType(String viewType) {
_registeredViewTypes.add(viewType); _registeredViewTypes.add(viewType);
} }
...@@ -165,6 +172,8 @@ class FakeAndroidPlatformViewsController { ...@@ -165,6 +172,8 @@ class FakeAndroidPlatformViewsController {
return _setDirection(call); return _setDirection(call);
case 'clearFocus': case 'clearFocus':
return _clearFocus(call); return _clearFocus(call);
case 'offset':
return _offset(call);
case 'synchronizeToNativeViewHierarchy': case 'synchronizeToNativeViewHierarchy':
return _synchronizeToNativeViewHierarchy(call); return _synchronizeToNativeViewHierarchy(call);
} }
...@@ -247,6 +256,15 @@ class FakeAndroidPlatformViewsController { ...@@ -247,6 +256,15 @@ class FakeAndroidPlatformViewsController {
} }
_views[id] = _views[id]!.copyWith(size: Size(width, height)); _views[id] = _views[id]!.copyWith(size: Size(width, height));
return Future<Map<dynamic, dynamic>>.sync(() => <dynamic, dynamic>{'width': width, 'height': height});
}
Future<dynamic> _offset(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
final int id = args['id'] as int;
final double top = args['top'] as double;
final double left = args['left'] as double;
offsets[id] = Offset(left, top);
return Future<dynamic>.sync(() => null); return Future<dynamic>.sync(() => null);
} }
......
...@@ -219,6 +219,28 @@ void main() { ...@@ -219,6 +219,28 @@ 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.
await viewController.setOffset(const Offset(10, 20));
expect(
viewsController.offsets,
equals(<int, Offset>{
7: const Offset(10, 20),
}),
);
});
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);
await viewController.setOffset(const Offset(10, 20));
expect(viewsController.offsets, equals(<int, Offset>{}));
});
test('synchronizeToNativeViewHierarchy', () async { test('synchronizeToNativeViewHierarchy', () async {
await PlatformViewsService.synchronizeToNativeViewHierarchy(false); await PlatformViewsService.synchronizeToNativeViewHierarchy(false);
expect(viewsController.synchronizeToNativeViewHierarchy, false); expect(viewsController.synchronizeToNativeViewHierarchy, false);
......
...@@ -1087,9 +1087,6 @@ void main() { ...@@ -1087,9 +1087,6 @@ void main() {
expect(lastPlatformViewTextClient.containsKey('platformViewId'), true); expect(lastPlatformViewTextClient.containsKey('platformViewId'), true);
expect(lastPlatformViewTextClient['platformViewId'], currentViewId + 1); expect(lastPlatformViewTextClient['platformViewId'], currentViewId + 1);
expect(lastPlatformViewTextClient.containsKey('usesVirtualDisplay'), true);
expect(lastPlatformViewTextClient['usesVirtualDisplay'], true);
}); });
testWidgets('AndroidView clears platform focus when unfocused', (WidgetTester tester) async { testWidgets('AndroidView clears platform focus when unfocused', (WidgetTester tester) async {
...@@ -1224,6 +1221,24 @@ void main() { ...@@ -1224,6 +1221,24 @@ void main() {
clipRectLayer = tester.layers.whereType<ClipRectLayer>().first; clipRectLayer = tester.layers.whereType<ClipRectLayer>().first;
expect(clipRectLayer.clipRect, const Rect.fromLTWH(0.0, 0.0, 50.0, 50.0)); expect(clipRectLayer.clipRect, const Rect.fromLTWH(0.0, 0.0, 50.0, 50.0));
}); });
testWidgets('offset is sent to the platform', (WidgetTester tester) async {
final FakeAndroidPlatformViewsController viewsController = FakeAndroidPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const Padding(
padding: EdgeInsets.fromLTRB(10, 20, 0, 0),
child: AndroidView(
viewType: 'webview',
layoutDirection: TextDirection.ltr,
),
),
);
await tester.pump();
expect(viewsController.offsets.values, equals(<Offset>[const Offset(10, 20)]));
});
}); });
group('AndroidViewSurface', () { group('AndroidViewSurface', () {
...@@ -2615,8 +2630,6 @@ void main() { ...@@ -2615,8 +2630,6 @@ void main() {
expect(focusNode.hasFocus, true); expect(focusNode.hasFocus, true);
expect(lastPlatformViewTextClient.containsKey('platformViewId'), true); expect(lastPlatformViewTextClient.containsKey('platformViewId'), true);
expect(lastPlatformViewTextClient['platformViewId'], viewId); expect(lastPlatformViewTextClient['platformViewId'], viewId);
expect(lastPlatformViewTextClient.containsKey('usesVirtualDisplay'), false);
}); });
}); });
......
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