Fix AndroidView offset and resize (#99888)
This commit is contained in:
parent
bb4a5fa7ab
commit
f58e8f56ef
@ -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 {
|
|||||||
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 {
|
|||||||
_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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
|
|
||||||
/// 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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
|||||||
@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?>(
|
||||||
'id': viewId,
|
'resize',
|
||||||
'width': size.width,
|
<String, dynamic>{
|
||||||
'height': size.height,
|
'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.
|
/// Creates the Android View.
|
||||||
@ -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> {
|
|||||||
}
|
}
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
_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() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user