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';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
......@@ -90,10 +91,15 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
updateGestureRecognizers(gestureRecognizers);
_viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
this.hitTestBehavior = hitTestBehavior;
_setOffset();
}
_PlatformViewState _state = _PlatformViewState.uninitialized;
Size? _currentTextureSize;
bool _isDisposed = false;
/// The Android view controller for the Android view associated with this render object.
AndroidViewController get viewController => _viewController;
AndroidViewController _viewController;
......@@ -172,8 +178,6 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
_sizePlatformView();
}
late Size _currentAndroidViewSize;
Future<void> _sizePlatformView() async {
// Android virtual displays cannot have a zero size.
// 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 {
Size targetSize;
do {
targetSize = size;
await _viewController.setSize(targetSize);
_currentAndroidViewSize = targetSize;
_currentTextureSize = await _viewController.setSize(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.
......@@ -199,14 +202,39 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
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
void paint(PaintingContext context, Offset offset) {
if (_viewController.textureId == null)
return;
// Clip the texture if it's going to paint out of the bounds of the renter box
// (see comment in _paintTexture for an explanation of when this happens).
if ((size.width < _currentAndroidViewSize.width || size.height < _currentAndroidViewSize.height) && clipBehavior != Clip.none) {
// As resizing the Android view happens asynchronously we don't know exactly when is a
// texture frame with the new size is ready for consumption.
// 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(
true,
offset,
......@@ -225,24 +253,18 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
@override
void dispose() {
_isDisposed = true;
_clipRectLayer.layer = null;
super.dispose();
}
void _paintTexture(PaintingContext context, Offset offset) {
// As resizing the Android view happens asynchronously we don't know exactly when is a
// texture frame with the new size is ready for consumption.
// 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.
if (_currentTextureSize == null)
return;
context.addLayer(TextureLayer(
rect: offset & _currentAndroidViewSize,
textureId: _viewController.textureId!,
freeze: _state == _PlatformViewState.resizing,
rect: offset & _currentTextureSize!,
textureId: viewController.textureId!,
));
}
......
......@@ -780,11 +780,27 @@ abstract class AndroidViewController extends PlatformViewController {
/// 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.
///
/// 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.
///
......@@ -975,15 +991,19 @@ class SurfaceAndroidViewController extends AndroidViewController {
}
@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.');
}
}
/// Controls an Android view that is rendered to a texture.
///
/// This is typically used by [AndroidView] to display an Android View in a
/// [VirtualDisplay](https://developer.android.com/reference/android/hardware/display/VirtualDisplay).
/// This is typically used by [AndroidView] to display an Android View in the Android view hierarchy.
///
/// Typically created with [PlatformViewsService.initAndroidView].
class TextureAndroidViewController extends AndroidViewController {
......@@ -1012,25 +1032,59 @@ class TextureAndroidViewController extends AndroidViewController {
@override
int? get textureId => _textureId;
late Size _size;
late Size _initialSize;
/// The current offset of the platform view.
Offset _off = Offset.zero;
@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(size != null);
assert(!size.isEmpty);
if (_state == _AndroidViewState.waitingForSize) {
_size = size;
return create();
_initialSize = size;
await create();
return _initialSize;
}
await SystemChannels.platform_views.invokeMethod<void>('resize', <String, dynamic>{
'id': viewId,
'width': size.width,
'height': size.height,
});
final Map<Object?, Object?>? meta = await SystemChannels.platform_views.invokeMapMethod<Object?, Object?>(
'resize',
<String, dynamic>{
'id': viewId,
'width': size.width,
'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.
......@@ -1043,13 +1097,13 @@ class TextureAndroidViewController extends AndroidViewController {
@override
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>{
'id': viewId,
'viewType': _viewType,
'width': _size.width,
'height': _size.height,
'width': _initialSize.width,
'height': _initialSize.height,
'direction': AndroidViewController._getAndroidDirection(_layoutDirection),
};
if (_creationParams != null) {
......
......@@ -546,7 +546,7 @@ class _AndroidViewState extends State<AndroidView> {
}
SystemChannels.textInput.invokeMethod<void>(
'TextInput.setPlatformViewClient',
<String, dynamic>{'platformViewId': _id, 'usesVirtualDisplay': true},
<String, dynamic>{'platformViewId': _id},
).catchError((dynamic e) {
if (e is MissingPluginException) {
// We land the framework part of Android platform views keyboard
......
......@@ -83,7 +83,12 @@ class FakeAndroidViewController implements AndroidViewController {
}
@override
Future<void> setSize(Size size) {
Future<Size> setSize(Size size) {
throw UnimplementedError();
}
@override
Future<void> setOffset(Offset off) {
throw UnimplementedError();
}
......@@ -140,6 +145,8 @@ class FakeAndroidPlatformViewsController {
bool synchronizeToNativeViewHierarchy = true;
Map<int, Offset> offsets = <int, Offset>{};
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
......@@ -165,6 +172,8 @@ class FakeAndroidPlatformViewsController {
return _setDirection(call);
case 'clearFocus':
return _clearFocus(call);
case 'offset':
return _offset(call);
case 'synchronizeToNativeViewHierarchy':
return _synchronizeToNativeViewHierarchy(call);
}
......@@ -247,6 +256,15 @@ class FakeAndroidPlatformViewsController {
}
_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);
}
......
......@@ -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 {
await PlatformViewsService.synchronizeToNativeViewHierarchy(false);
expect(viewsController.synchronizeToNativeViewHierarchy, false);
......
......@@ -1087,9 +1087,6 @@ void main() {
expect(lastPlatformViewTextClient.containsKey('platformViewId'), true);
expect(lastPlatformViewTextClient['platformViewId'], currentViewId + 1);
expect(lastPlatformViewTextClient.containsKey('usesVirtualDisplay'), true);
expect(lastPlatformViewTextClient['usesVirtualDisplay'], true);
});
testWidgets('AndroidView clears platform focus when unfocused', (WidgetTester tester) async {
......@@ -1224,6 +1221,24 @@ void main() {
clipRectLayer = tester.layers.whereType<ClipRectLayer>().first;
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', () {
......@@ -2615,8 +2630,6 @@ void main() {
expect(focusNode.hasFocus, true);
expect(lastPlatformViewTextClient.containsKey('platformViewId'), true);
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