Unverified Commit bc994c7f authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Allow Hybrid Composition fallback for Android platform views (#109161)

parent de517093
......@@ -1070,10 +1070,79 @@ class PlatformViewSurface extends LeafRenderObjectWidget {
///
/// * [AndroidView] which embeds an Android platform view in the widget hierarchy.
/// * [UiKitView] which embeds an iOS platform view in the widget hierarchy.
class AndroidViewSurface extends PlatformViewSurface {
class AndroidViewSurface extends StatefulWidget {
/// Construct an `AndroidPlatformViewSurface`.
const AndroidViewSurface({
super.key,
required this.controller,
required this.hitTestBehavior,
required this.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null);
/// The controller for the platform view integrated by this [AndroidViewSurface].
///
/// See [PlatformViewSurface.controller] for details.
final AndroidViewController controller;
/// Which gestures should be forwarded to the PlatformView.
///
/// See [PlatformViewSurface.gestureRecognizers] for details.
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
/// {@macro flutter.widgets.AndroidView.hitTestBehavior}
final PlatformViewHitTestBehavior hitTestBehavior;
@override
State<StatefulWidget> createState() {
return _AndroidViewSurfaceState();
}
}
class _AndroidViewSurfaceState extends State<AndroidViewSurface> {
@override
void initState() {
super.initState();
if (!widget.controller.isCreated) {
// Schedule a rebuild once creation is complete and the final dislay
// type is known.
widget.controller.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
}
}
@override
void dispose() {
widget.controller.removeOnPlatformViewCreatedListener(_onPlatformViewCreated);
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.controller.requiresViewComposition) {
return _PlatformLayerBasedAndroidViewSurface(
controller: widget.controller,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers,
);
} else {
return _TextureBasedAndroidViewSurface(
controller: widget.controller,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers,
);
}
}
void _onPlatformViewCreated(int _) {
// Trigger a re-build based on the current controller state.
setState(() {});
}
}
// Displays an Android platform view via GL texture.
class _TextureBasedAndroidViewSurface extends PlatformViewSurface {
const _TextureBasedAndroidViewSurface({
required AndroidViewController super.controller,
required super.hitTestBehavior,
required super.gestureRecognizers,
......@@ -1084,16 +1153,6 @@ class AndroidViewSurface extends PlatformViewSurface {
@override
RenderObject createRenderObject(BuildContext context) {
final AndroidViewController viewController = controller as AndroidViewController;
// Compose using the Android view hierarchy.
// This is useful when embedding a SurfaceView into a Flutter app.
// 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(
......@@ -1107,6 +1166,26 @@ class AndroidViewSurface extends PlatformViewSurface {
}
}
class _PlatformLayerBasedAndroidViewSurface extends PlatformViewSurface {
const _PlatformLayerBasedAndroidViewSurface({
required AndroidViewController super.controller,
required super.hitTestBehavior,
required super.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null);
@override
RenderObject createRenderObject(BuildContext context) {
final AndroidViewController viewController = controller as AndroidViewController;
final PlatformViewRenderBox renderBox =
super.createRenderObject(context) as PlatformViewRenderBox;
viewController.pointTransformer =
(Offset position) => renderBox.globalToLocal(position);
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);
......
......@@ -45,7 +45,11 @@ class FakePlatformViewController extends PlatformViewController {
}
class FakeAndroidViewController implements AndroidViewController {
FakeAndroidViewController(this.viewId, {this.requiresSize = false});
FakeAndroidViewController(
this.viewId, {
this.requiresSize = false,
this.requiresViewComposition = false,
});
bool disposed = false;
bool focusCleared = false;
......@@ -56,6 +60,8 @@ class FakeAndroidViewController implements AndroidViewController {
bool _createCalledSuccessfully = false;
final List<PlatformViewCreatedCallback> _createdCallbacks = <PlatformViewCreatedCallback>[];
/// Events that are dispatched.
List<PointerEvent> dispatchedPointerEvents = <PointerEvent>[];
......@@ -106,10 +112,13 @@ class FakeAndroidViewController implements AndroidViewController {
@override
void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
created = true;
createdCallbacks.add(listener);
}
@override
void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {}
void removeOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) {
createdCallbacks.remove(listener);
}
@override
Future<void> sendMotionEvent(AndroidMotionEvent event) {
......@@ -128,7 +137,10 @@ class FakeAndroidViewController implements AndroidViewController {
}
@override
List<PlatformViewCreatedCallback> get createdCallbacks => <PlatformViewCreatedCallback>[];
List<PlatformViewCreatedCallback> get createdCallbacks => _createdCallbacks;
@override
bool requiresViewComposition;
}
class FakeAndroidPlatformViewsController {
......@@ -153,6 +165,11 @@ class FakeAndroidPlatformViewsController {
Map<int, Offset> offsets = <int, Offset>{};
/// True if Texture Layer Hybrid Composition mode should be enabled.
///
/// When false, `create` will simulate the engine's fallback mode.
bool allowTextureLayerMode = true;
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
......@@ -192,6 +209,7 @@ class FakeAndroidPlatformViewsController {
final double? height = args['height'] as double?;
final int layoutDirection = args['direction'] as int;
final bool? hybrid = args['hybrid'] as bool?;
final bool? hybridFallback = args['hybridFallback'] as bool?;
final Uint8List? creationParams = args['params'] as Uint8List?;
if (_views.containsKey(id)) {
......@@ -215,11 +233,21 @@ class FakeAndroidPlatformViewsController {
_views[id] = FakeAndroidPlatformView(id, viewType,
width != null && height != null ? Size(width, height) : null,
layoutDirection,
hybrid,
creationParams,
hybrid: hybrid,
hybridFallback: hybridFallback,
creationParams: creationParams,
);
// Return a hybrid result (null rather than a texture ID) if:
final bool hybridResult =
// hybrid was explicitly requested, or
(hybrid ?? false) ||
// hybrid fallback was requested and simulated.
(!allowTextureLayerMode && (hybridFallback ?? false));
if (hybridResult) {
return Future<void>.value();
}
final int textureId = _textureCounter++;
return Future<int>.sync(() => textureId);
return Future<int>.value(textureId);
}
Future<dynamic> _dispose(MethodCall call) {
......@@ -506,7 +534,8 @@ class FakeHtmlPlatformViewsController {
@immutable
class FakeAndroidPlatformView {
const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, this.hybrid, [this.creationParams]);
const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection,
{this.hybrid, this.hybridFallback, this.creationParams});
final int id;
final String type;
......@@ -514,14 +543,16 @@ class FakeAndroidPlatformView {
final Size? size;
final int layoutDirection;
final bool? hybrid;
final bool? hybridFallback;
FakeAndroidPlatformView copyWith({Size? size, int? layoutDirection}) => FakeAndroidPlatformView(
id,
type,
size ?? this.size,
layoutDirection ?? this.layoutDirection,
hybrid,
creationParams,
hybrid: hybrid,
hybridFallback: hybridFallback,
creationParams: creationParams,
);
@override
......@@ -535,6 +566,7 @@ class FakeAndroidPlatformView {
&& listEquals<int>(other.creationParams, creationParams)
&& other.size == size
&& other.hybrid == hybrid
&& other.hybridFallback == hybridFallback
&& other.layoutDirection == layoutDirection;
}
......@@ -546,11 +578,14 @@ class FakeAndroidPlatformView {
size,
layoutDirection,
hybrid,
hybridFallback,
);
@override
String toString() {
return 'FakeAndroidPlatformView(id: $id, type: $type, size: $size, layoutDirection: $layoutDirection, hybrid: $hybrid, creationParams: $creationParams)';
return 'FakeAndroidPlatformView(id: $id, type: $type, size: $size, '
'layoutDirection: $layoutDirection, hybrid: $hybrid, '
'hybridFallback: $hybridFallback, creationParams: $creationParams)';
}
}
......
......@@ -48,7 +48,7 @@ void main() {
}
});
test('create Android views', () async {
test('create VD-fallback Android views', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr)
.create(size: const Size(100.0, 100.0));
......@@ -57,12 +57,85 @@ void main() {
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
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(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
const FakeAndroidPlatformView(1, 'webview', Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl),
]),
);
});
test('create HC-fallback Android views', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initSurfaceAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr)
.create(size: const Size(100.0, 100.0));
await PlatformViewsService.initSurfaceAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl)
.create(size: const Size(200.0, 300.0));
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr,
hybridFallback: true),
const FakeAndroidPlatformView(1, 'webview', Size(200.0, 300.0), AndroidViewController.kAndroidLayoutDirectionRtl,
hybridFallback: true),
]),
);
});
test('create HC-only Android views', () async {
viewsController.registerViewType('webview');
await PlatformViewsService.initExpensiveAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr)
.create(size: const Size(100.0, 100.0));
await PlatformViewsService.initExpensiveAndroidView( id: 1, viewType: 'webview', layoutDirection: TextDirection.rtl)
.create(size: const Size(200.0, 300.0));
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
const FakeAndroidPlatformView(0, 'webview', null, AndroidViewController.kAndroidLayoutDirectionLtr,
hybrid: true),
const FakeAndroidPlatformView(1, 'webview', null, AndroidViewController.kAndroidLayoutDirectionRtl,
hybrid: true),
]),
);
});
test('default view does not use view composition by default', () async {
viewsController.registerViewType('webview');
final AndroidViewController controller = PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
await controller.create(size: const Size(100.0, 100.0));
expect(controller.requiresViewComposition, false);
});
test('default view does not use view composition in fallback mode', () async {
viewsController.registerViewType('webview');
viewsController.allowTextureLayerMode = false;
final AndroidViewController controller = PlatformViewsService.initAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
await controller.create(size: const Size(100.0, 100.0));
viewsController.allowTextureLayerMode = true;
expect(controller.requiresViewComposition, false);
});
test('surface view does not use view composition by default', () async {
viewsController.registerViewType('webview');
final AndroidViewController controller = PlatformViewsService.initSurfaceAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
await controller.create(size: const Size(100.0, 100.0));
expect(controller.requiresViewComposition, false);
});
test('surface view does uses view composition in fallback mode', () async {
viewsController.registerViewType('webview');
viewsController.allowTextureLayerMode = false;
final AndroidViewController controller = PlatformViewsService.initSurfaceAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
await controller.create(size: const Size(100.0, 100.0));
viewsController.allowTextureLayerMode = true;
expect(controller.requiresViewComposition, true);
});
test('expensive view uses view composition', () async {
viewsController.registerViewType('webview');
final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView(id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr);
await controller.create(size: const Size(100.0, 100.0));
expect(controller.requiresViewComposition, true);
});
test('reuse Android view id', () async {
viewsController.registerViewType('webview');
final AndroidViewController controller = PlatformViewsService.initAndroidView(
......@@ -111,7 +184,7 @@ void main() {
expect(
viewsController.views,
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),
]),
);
});
......@@ -164,8 +237,8 @@ void main() {
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr, null),
const FakeAndroidPlatformView(1, 'webview', Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr, null),
const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionLtr),
const FakeAndroidPlatformView(1, 'webview', Size(500.0, 500.0), AndroidViewController.kAndroidLayoutDirectionLtr),
]),
);
});
......@@ -209,7 +282,7 @@ void main() {
expect(
viewsController.views,
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),
]),
);
});
......@@ -226,7 +299,7 @@ void main() {
expect(
viewsController.views,
unorderedEquals(<FakeAndroidPlatformView>[
const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl, null),
const FakeAndroidPlatformView(0, 'webview', Size(100.0, 100.0), AndroidViewController.kAndroidLayoutDirectionRtl),
]),
);
});
......
......@@ -41,7 +41,6 @@ void main() {
'webview',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -85,8 +84,7 @@ void main() {
'webview',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
fakeView.creationParams,
creationParams: fakeView.creationParams,
),
]),
);
......@@ -150,7 +148,6 @@ void main() {
'webview',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -166,7 +163,6 @@ void main() {
'webview',
const Size(100.0, 50.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -205,7 +201,6 @@ void main() {
'maps',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -272,7 +267,6 @@ void main() {
'webview',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -495,7 +489,6 @@ void main() {
'maps',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionRtl,
null,
),
]),
);
......@@ -518,7 +511,6 @@ void main() {
'maps',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -549,7 +541,6 @@ void main() {
'maps',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionRtl,
null,
),
]),
);
......@@ -575,7 +566,6 @@ void main() {
'maps',
const Size(200.0, 100.0),
AndroidViewController.kAndroidLayoutDirectionLtr,
null,
),
]),
);
......@@ -1257,6 +1247,63 @@ void main() {
await tester.pumpWidget(surface);
expect(controller.pointTransformer, isNotNull);
});
testWidgets('AndroidViewSurface defaults to texture-based rendering', (WidgetTester tester) async {
final AndroidViewSurface surface = AndroidViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
);
await tester.pumpWidget(surface);
expect(find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_TextureBasedAndroidViewSurface',
), findsOneWidget);
});
testWidgets('AndroidViewSurface uses view-based rendering when initially required', (WidgetTester tester) async {
controller.requiresViewComposition = true;
final AndroidViewSurface surface = AndroidViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
);
await tester.pumpWidget(surface);
expect(find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_PlatformLayerBasedAndroidViewSurface',
), findsOneWidget);
});
testWidgets('AndroidViewSurface can switch to view-based rendering after creation', (WidgetTester tester) async {
final AndroidViewSurface surface = AndroidViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
);
await tester.pumpWidget(surface);
expect(find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_TextureBasedAndroidViewSurface',
), findsOneWidget);
expect(find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_PlatformLayerBasedAndroidViewSurface',
), findsNothing);
// Simulate a creation-time switch to view composition.
controller.requiresViewComposition = true;
for (final PlatformViewCreatedCallback callback in controller.createdCallbacks) {
callback(controller.viewId);
}
await tester.pumpWidget(surface);
expect(find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_TextureBasedAndroidViewSurface',
), findsNothing);
expect(find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_PlatformLayerBasedAndroidViewSurface',
), findsOneWidget);
});
});
group('UiKitView', () {
......
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