[Android] HC++ wire up dart platform channel code and integration test. (#162751)

Use the PlatformViewController2 to register a platform view, allow the
dart side platform view logic to opt into this new platform view. Wires
up an integration test with android_engine_test.
This commit is contained in:
Jonah Williams 2025-02-06 19:09:24 -08:00 committed by GitHub
parent 7569fbfce5
commit f0396970e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 477 additions and 18 deletions

View File

@ -57,7 +57,7 @@ Future<void> runAndroidEngineTests({required ImpellerBackend impellerBackend}) a
// TODO(matanlurey): Enable once `flutter drive` retains error logs.
// final RegExp impellerStdoutPattern = RegExp('Using the Imepller rendering backend (.*)');
for (final FileSystemEntity file in mains) {
Future<void> runTest(FileSystemEntity file) async {
final CommandResult result = await runCommand(
'flutter',
<String>[
@ -77,7 +77,7 @@ Future<void> runAndroidEngineTests({required ImpellerBackend impellerBackend}) a
final String? stdout = result.flattenedStdout;
if (stdout == null) {
foundError(<String>['No stdout produced.']);
continue;
return;
}
// TODO(matanlurey): Enable once `flutter drive` retains error logs.
@ -86,7 +86,7 @@ Future<void> runAndroidEngineTests({required ImpellerBackend impellerBackend}) a
// final Match? stdoutMatch = impellerStdoutPattern.firstMatch(stdout);
// if (stdoutMatch == null) {
// foundError(<String>['Could not find pattern ${impellerStdoutPattern.pattern}.', stdout]);
// continue;
// return;
// }
// final String reportedBackend = stdoutMatch.group(1)!.toLowerCase();
@ -94,15 +94,43 @@ Future<void> runAndroidEngineTests({required ImpellerBackend impellerBackend}) a
// foundError(<String>[
// 'Reported Imepller backend was $reportedBackend, expected ${impellerBackend.name}',
// ]);
// continue;
// return;
// }
}
for (final FileSystemEntity file in mains) {
if (file.path.contains('hcpp')) {
continue;
}
await runTest(file);
}
// Test HCPP Platform Views on Vulkan.
if (impellerBackend == ImpellerBackend.vulkan) {
androidManifestXml.writeAsStringSync(
androidManifestXml.readAsStringSync().replaceFirst(
kSurfaceControlMetadataDisabled,
kSurfaceControlMetadataEnabled,
),
);
for (final FileSystemEntity file in mains) {
if (!file.path.contains('hcpp')) {
continue;
}
await runTest(file);
}
}
} finally {
// Restore original contents.
androidManifestXml.writeAsStringSync(androidManifestContents);
}
}
const String kSurfaceControlMetadataDisabled =
'<meta-data android:name="io.flutter.embedding.android.UseSurfaceControl" android:value="false" />';
const String kSurfaceControlMetadataEnabled =
'<meta-data android:name="io.flutter.embedding.android.UseSurfaceControl" android:value="true" />';
String _impellerBackendMetadata({required String value}) {
return '<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="$value" />';
}

View File

@ -21,12 +21,12 @@ found in the LICENSE file. -->
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@ -34,6 +34,7 @@ found in the LICENSE file. -->
<meta-data android:name="flutterEmbedding" android:value="2" />
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="true" />
<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="vulkan" />
<meta-data android:name="io.flutter.embedding.android.EnableSurfaceControl" android:value="false" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import com.example.android_engine_test.extensions.NativeDriverSupportPlugin
import com.example.android_engine_test.fixtures.BlueOrangeGradientPlatformViewFactory
import com.example.android_engine_test.fixtures.BlueOrangeGradientSurfaceViewPlatformViewFactory
import com.example.android_engine_test.fixtures.BoxPlatformViewFactory
import com.example.android_engine_test.fixtures.ChangingColorButtonPlatformViewFactory
import com.example.android_engine_test.fixtures.OtherFaceTexturePlugin
import com.example.android_engine_test.fixtures.SmileyFaceTexturePlugin
@ -31,6 +32,17 @@ class MainActivity : FlutterActivity() {
add(NativeDriverSupportPlugin())
}
// TODO(jonahwilliams): make platform view controllers share platform view registry.
flutterEngine
.platformViewsController2
.registry
.apply {
registerViewFactory("blue_orange_gradient_platform_view", BlueOrangeGradientPlatformViewFactory())
registerViewFactory("blue_orange_gradient_surface_view_platform_view", BlueOrangeGradientSurfaceViewPlatformViewFactory())
registerViewFactory("changing_color_button_platform_view", ChangingColorButtonPlatformViewFactory())
registerViewFactory("box_platform_view", BoxPlatformViewFactory())
}
flutterEngine
.platformViewsController
.registry
@ -38,6 +50,7 @@ class MainActivity : FlutterActivity() {
registerViewFactory("blue_orange_gradient_platform_view", BlueOrangeGradientPlatformViewFactory())
registerViewFactory("blue_orange_gradient_surface_view_platform_view", BlueOrangeGradientSurfaceViewPlatformViewFactory())
registerViewFactory("changing_color_button_platform_view", ChangingColorButtonPlatformViewFactory())
registerViewFactory("box_platform_view", BoxPlatformViewFactory())
}
}

View File

@ -0,0 +1,58 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@file:Suppress("PackageName")
package com.example.android_engine_test.fixtures
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.view.View
import android.view.ViewGroup
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
class BoxPlatformViewFactory : PlatformViewFactory(null) {
override fun create(
context: Context,
viewId: Int,
args: Any?
): PlatformView = BoxPlatformView(context)
}
private class BoxPlatformView(
context: Context
) : View(context),
PlatformView {
val paint = Paint()
init {
layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun getView(): View = this
override fun dispose() {}
override fun onDraw(canvas: Canvas) {
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
super.onDraw(canvas)
}
override fun onSizeChanged(
w: Int,
h: Int,
oldw: Int,
oldh: Int
) {
paint.color = Color.rgb(0x41, 0x69, 0xE1)
super.onSizeChanged(w, h, oldw, oldh)
}
}

View File

@ -0,0 +1,78 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:android_driver_extensions/extension.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_driver/driver_extension.dart';
import '../src/allow_list_devices.dart';
void main() async {
ensureAndroidDevice();
enableFlutterDriverExtension(commands: <CommandExtension>[nativeDriverCommands]);
// Run on full screen.
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
runApp(const MainApp());
}
final class MainApp extends StatelessWidget {
const MainApp({super.key});
// This should appear as the yellow line over a blue box. The
// red box should not be visible unless the platform view has not loaded yet.
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
SizedBox(width: 190, height: 190, child: ColoredBox(color: Colors.red)),
SizedBox(
width: 200,
height: 200,
child: _HybridCompositionAndroidPlatformView(viewType: 'box_platform_view'),
),
SizedBox(width: 800, height: 25, child: ColoredBox(color: Colors.yellow)),
],
),
);
}
}
final class _HybridCompositionAndroidPlatformView extends StatelessWidget {
const _HybridCompositionAndroidPlatformView({required this.viewType});
final String viewType;
// TODO(jonahwilliams): swap this out with new platform view APIs.
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: viewType,
surfaceFactory: (BuildContext context, PlatformViewController controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initHybridAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParamsCodec: const StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
}

View File

@ -0,0 +1,66 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:android_driver_extensions/native_driver.dart';
import 'package:android_driver_extensions/skia_gold.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
import '../_luci_skia_gold_prelude.dart';
/// For local debugging, a (local) golden-file is required as a baseline:
///
/// ```sh
/// # Checkout HEAD, i.e. *before* changes you want to test.
/// UPDATE_GOLDENS=1 flutter drive lib/platform_view/hcpp/platform_view_main.dart
///
/// # Make your changes.
///
/// # Run the test against baseline.
/// flutter drive lib/platform_view/hcpp/platform_view_main.dart
/// ```
///
/// For a convenient way to deflake a test, see `tool/deflake.dart`.
void main() async {
const String goldenPrefix = 'hybrid_composition_pp_platform_view';
late final FlutterDriver flutterDriver;
late final NativeDriver nativeDriver;
setUpAll(() async {
if (isLuci) {
await enableSkiaGoldComparator(namePrefix: 'android_engine_test$goldenVariant');
}
flutterDriver = await FlutterDriver.connect();
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
await nativeDriver.configureForScreenshotTesting();
await flutterDriver.waitUntilFirstFrameRasterized();
});
tearDownAll(() async {
await nativeDriver.close();
await flutterDriver.close();
});
test('should screenshot an HCPP platform view', () async {
await expectLater(
nativeDriver.screenshot(),
matchesGoldenFile('$goldenPrefix.platform_view.png'),
);
}, timeout: Timeout.none);
test('should rotate landscape and screenshot the platform view', () async {
await nativeDriver.rotateToLandscape();
await expectLater(
nativeDriver.screenshot(),
matchesGoldenFile('$goldenPrefix.platform_view_landscape_rotated.png'),
);
await nativeDriver.rotateResetDefault();
await expectLater(
nativeDriver.screenshot(),
matchesGoldenFile('$goldenPrefix.platform_view_portait_rotated_back.png'),
);
}, timeout: Timeout.none);
}

View File

@ -110,6 +110,7 @@ class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI {
int32_t viewHeight,
MutatorsStack mutators_stack),
(override));
MOCK_METHOD(void, onEndFrame2, (), (override));
MOCK_METHOD(std::unique_ptr<std::vector<std::string>>,
FlutterViewComputePlatformResolvedLocale,
(std::vector<std::string> supported_locales_data),

View File

@ -3,10 +3,12 @@
// found in the LICENSE file.
#include "flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.h"
#include "display_list/dl_color.h"
#include "flow/view_slicer.h"
#include "flutter/fml/synchronization/waitable_event.h"
#include "flutter/fml/trace_event.h"
#include "fml/make_copyable.h"
#include "fml/synchronization/count_down_latch.h"
namespace flutter {
@ -90,8 +92,24 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
view_rects //
);
// Create Overlay frame.
surface_pool_->TrimLayers();
// If there is no overlay Surface, initialize one on the platform thread. This
// will only be done once per application launch, as the singular overlay
// surface is never released.
surface_pool_->ResetLayers();
if (!surface_pool_->HasLayers()) {
std::shared_ptr<fml::CountDownLatch> latch =
std::make_shared<fml::CountDownLatch>(1u);
task_runners_.GetPlatformTaskRunner()->PostTask(
fml::MakeCopyable([&, latch]() {
surface_pool_->GetLayer(context, android_context_, jni_facade_,
surface_factory_);
latch->CountDown();
}));
latch->Wait();
}
// Create Overlay frame. If overlay surface creation failed,
// all this work must be skipped.
std::unique_ptr<SurfaceFrame> overlay_frame;
if (surface_pool_->HasLayers()) {
for (int64_t view_id : composition_order_) {
@ -127,6 +145,7 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
jni_facade = jni_facade_, device_pixel_ratio = device_pixel_ratio_,
slices = std::move(slices_)]() -> void {
jni_facade->swapTransaction();
for (int64_t view_id : composition_order) {
SkRect view_rect = GetViewRect(view_id, view_params);
const EmbeddedViewParams& params = view_params.at(view_id);
@ -141,10 +160,7 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
params.mutatorsStack() //
);
}
if (!surface_pool_->HasLayers()) {
surface_pool_->GetLayer(context, android_context_, jni_facade_,
surface_factory_);
}
jni_facade_->onEndFrame2();
}));
}

View File

@ -122,6 +122,10 @@ void SurfacePool::SetFrameSize(SkISize frame_size) {
requested_frame_size_ = frame_size;
}
void SurfacePool::ResetLayers() {
available_layer_index_ = 0;
}
void SurfacePool::TrimLayers() {
std::lock_guard lock(mutex_);
layers_.erase(layers_.begin() + available_layer_index_, layers_.end());

View File

@ -76,6 +76,9 @@ class SurfacePool {
// Returns true if the current pool has layers in use.
bool HasLayers();
// Reset the layer index but do not release any layers.
void ResetLayers();
void TrimLayers();
private:

View File

@ -378,10 +378,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
attachToJni();
}
// TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if
// possible.
this.renderer = new FlutterRenderer(flutterJNI);
this.platformViewsController = platformViewsController;
this.platformViewsController2 = platformViewsController2;

View File

@ -341,6 +341,7 @@ import java.util.Set;
flutterEngine
.getPlatformViewsController()
.attach(activity, flutterEngine.getRenderer(), flutterEngine.getDartExecutor());
flutterEngine.getPlatformViewsController2().attach(activity, flutterEngine.getDartExecutor());
// Notify all ActivityAware plugins that they are now attached to a new Activity.
for (ActivityAware activityAware : activityAwarePlugins.values()) {
@ -391,6 +392,7 @@ import java.util.Set;
private void detachFromActivityInternal() {
// Deactivate PlatformViewsController.
flutterEngine.getPlatformViewsController().detach();
flutterEngine.getPlatformViewsController2().detach();
exclusiveActivity = null;
activityPluginBinding = null;

View File

@ -1301,6 +1301,16 @@ public class FlutterJNI {
platformViewsController2.applyTransactions();
}
@SuppressWarnings("unused")
@SuppressLint("NewApi")
@UiThread
public void endFrame2() {
if (platformViewsController2 == null) {
throw new RuntimeException("");
}
platformViewsController2.onEndFrame();
}
@SuppressWarnings("unused")
@SuppressLint("NewApi")
@UiThread

View File

@ -560,6 +560,7 @@ public class PlatformViewsController2 implements PlatformViewsAccessibilityDeleg
surfaceControlBuilder.setFormat(PixelFormat.RGBA_8888);
surfaceControlBuilder.setName("Flutter Overlay Surface");
surfaceControlBuilder.setOpaque(false);
surfaceControlBuilder.setHidden(false);
final SurfaceControl surfaceControl = surfaceControlBuilder.build();
final SurfaceControl.Transaction tx =
flutterView.getRootSurfaceControl().buildReparentTransaction(surfaceControl);

View File

@ -139,6 +139,8 @@ class JNIMock final : public PlatformViewAndroidJNI {
MutatorsStack mutators_stack),
(override));
MOCK_METHOD(void, onEndFrame2, (), (override));
MOCK_METHOD(std::unique_ptr<std::vector<std::string>>,
FlutterViewComputePlatformResolvedLocale,
(std::vector<std::string> supported_locales_data),

View File

@ -228,6 +228,8 @@ class PlatformViewAndroidJNI {
virtual void destroyOverlaySurface2() = 0;
virtual void onEndFrame2() = 0;
virtual void onDisplayPlatformView2(int32_t view_id,
int32_t x,
int32_t y,

View File

@ -158,6 +158,8 @@ static jmethodID g_destroy_overlay_surface2_method = nullptr;
static jmethodID g_on_display_platform_view2_method = nullptr;
static jmethodID g_on_end_frame2_method = nullptr;
// Mutators
static fml::jni::ScopedJavaGlobalRef<jclass>* g_mutators_stack_class = nullptr;
static jmethodID g_mutators_stack_init_method = nullptr;
@ -1032,6 +1034,13 @@ bool RegisterApi(JNIEnv* env) {
FML_LOG(ERROR) << "Could not locate onDisplayPlatformView2 method";
return false;
}
g_on_end_frame2_method =
env->GetMethodID(g_flutter_jni_class->obj(), "endFrame2", "()V");
if (g_on_end_frame2_method == nullptr) {
FML_LOG(ERROR) << "Could not locate onEndFrame2 method";
return false;
}
//
fml::jni::ScopedJavaLocalRef<jclass> overlay_surface_class(
@ -2151,4 +2160,17 @@ void PlatformViewAndroidJNIImpl::onDisplayPlatformView2(
FML_CHECK(fml::jni::CheckException(env));
}
void PlatformViewAndroidJNIImpl::onEndFrame2() {
JNIEnv* env = fml::jni::AttachCurrentThread();
auto java_object = java_object_.get(env);
if (java_object.is_null()) {
return;
}
env->CallVoidMethod(java_object.obj(), g_on_end_frame2_method);
FML_CHECK(fml::jni::CheckException(env));
}
} // namespace flutter

View File

@ -124,6 +124,8 @@ class PlatformViewAndroidJNIImpl final : public PlatformViewAndroidJNI {
int32_t viewHeight,
MutatorsStack mutators_stack) override;
void onEndFrame2() override;
private:
// Reference to FlutterJNI object.
const fml::jni::JavaObjectWeakGlobalRef java_object_;

View File

@ -21,6 +21,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -37,6 +38,8 @@ public class FlutterEngineConnectionRegistryTest {
FlutterEngine flutterEngine = mock(FlutterEngine.class);
PlatformViewsController platformViewsController = mock(PlatformViewsController.class);
when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController);
PlatformViewsController2 platformViewsController2 = mock(PlatformViewsController2.class);
when(flutterEngine.getPlatformViewsController2()).thenReturn(platformViewsController2);
FlutterLoader flutterLoader = mock(FlutterLoader.class);
@ -75,6 +78,8 @@ public class FlutterEngineConnectionRegistryTest {
FlutterEngine flutterEngine = mock(FlutterEngine.class);
PlatformViewsController platformViewsController = mock(PlatformViewsController.class);
when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController);
PlatformViewsController2 platformViewsController2 = mock(PlatformViewsController2.class);
when(flutterEngine.getPlatformViewsController2()).thenReturn(platformViewsController2);
FlutterLoader flutterLoader = mock(FlutterLoader.class);
@ -124,6 +129,8 @@ public class FlutterEngineConnectionRegistryTest {
FlutterEngine flutterEngine = mock(FlutterEngine.class);
PlatformViewsController platformViewsController = mock(PlatformViewsController.class);
when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController);
PlatformViewsController2 platformViewsController2 = mock(PlatformViewsController2.class);
when(flutterEngine.getPlatformViewsController2()).thenReturn(platformViewsController2);
FlutterLoader flutterLoader = mock(FlutterLoader.class);

View File

@ -208,6 +208,33 @@ class PlatformViewsService {
return controller;
}
/// {@macro flutter.services.PlatformViewsService.initAndroidView}
///
/// When this factory is used, the Android view and Flutter widgets are
/// composed at the Android view hierarchy level.
///
/// This functionality is only supported on Android devices running Vulkan on
/// API 34 or newer.
static HybridAndroidViewController initHybridAndroidView({
required int id,
required String viewType,
required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) {
final HybridAndroidViewController controller = HybridAndroidViewController._(
viewId: id,
viewType: viewType,
layoutDirection: layoutDirection,
creationParams: creationParams,
creationParamsCodec: creationParamsCodec,
);
_instance._focusCallbacks[id] = onFocus ?? () {};
return controller;
}
/// Factory method to create a `UiKitView`.
///
/// The `id` parameter is an unused unique identifier generated with
@ -861,6 +888,12 @@ abstract class AndroidViewController extends PlatformViewController {
/// call's future has completed.
bool get requiresViewComposition => false;
/// True if the experimental hybrid composition controller is enabled.
///
/// This value may change during [create], but will not change after that
/// call's future has completed.
bool get useNewHybridComposition => false;
/// Sends an Android [MotionEvent](https://developer.android.com/reference/android/view/MotionEvent)
/// to the view.
///
@ -1117,6 +1150,64 @@ class ExpensiveAndroidViewController extends AndroidViewController {
}
}
/// Controls an Android view that is composed using the Android view hierarchy.
/// This controller is created from the [PlatformViewsService.initExpensiveAndroidView] factory.
class HybridAndroidViewController extends AndroidViewController {
HybridAndroidViewController._({
required super.viewId,
required super.viewType,
required super.layoutDirection,
super.creationParams,
super.creationParamsCodec,
}) : super._();
final _AndroidViewControllerInternals _internals = _Hybrid2AndroidViewControllerInternals();
@override
bool get _createRequiresSize => false;
@override
Future<void> _sendCreateMessage({required Size? size, Offset? position}) async {
await _AndroidViewControllerInternals.sendCreateMessage(
viewId: viewId,
viewType: _viewType,
hybrid: true,
layoutDirection: _layoutDirection,
creationParams: _creationParams,
position: position,
useNewController: true,
);
}
@override
bool get useNewHybridComposition => true;
@override
int? get textureId {
return _internals.textureId;
}
@override
bool get requiresViewComposition {
return _internals.requiresViewComposition;
}
@override
Future<void> _sendDisposeMessage() {
return _internals.sendDisposeMessage(viewId: viewId);
}
@override
Future<Size> _sendResizeMessage(Size size) {
return _internals.setSize(size, viewId: viewId, viewState: _state);
}
@override
Future<void> setOffset(Offset off) {
return _internals.setOffset(off, viewId: viewId, viewState: _state);
}
}
/// 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.
///
@ -1201,6 +1292,7 @@ abstract class _AndroidViewControllerInternals {
required TextDirection layoutDirection,
required bool hybrid,
bool hybridFallback = false,
bool useNewController = false,
_CreationParams? creationParams,
Size? size,
Offset? position,
@ -1220,6 +1312,9 @@ abstract class _AndroidViewControllerInternals {
final ByteData paramsByteData = creationParams.codec.encodeMessage(creationParams.data)!;
args['params'] = Uint8List.view(paramsByteData.buffer, 0, paramsByteData.lengthInBytes);
}
if (useNewController) {
return SystemChannels.platform_views_2.invokeMethod<dynamic>('create', args);
}
return SystemChannels.platform_views.invokeMethod<dynamic>('create', args);
}
@ -1349,6 +1444,38 @@ class _HybridAndroidViewControllerInternals extends _AndroidViewControllerIntern
}
}
class _Hybrid2AndroidViewControllerInternals extends _AndroidViewControllerInternals {
@override
int get textureId {
throw UnimplementedError('Not supported for hybrid composition.');
}
@override
bool get requiresViewComposition => true;
@override
Future<Size> setSize(Size size, {required int viewId, required _AndroidViewState viewState}) {
throw UnimplementedError('Not supported for hybrid composition.');
}
@override
Future<void> setOffset(
Offset offset, {
required int viewId,
required _AndroidViewState viewState,
}) {
throw UnimplementedError('Not supported for hybrid composition.');
}
@override
Future<void> sendDisposeMessage({required int viewId}) {
return SystemChannels.platform_views_2.invokeMethod<void>('dispose', <String, dynamic>{
'id': viewId,
'hybrid': true,
});
}
}
/// Base class for iOS and macOS view controllers.
///
/// View controllers are used to create and interact with the UIView or NSView

View File

@ -401,6 +401,13 @@ abstract final class SystemChannels {
/// * [PlatformViewsService] for the available operations on this channel.
static const MethodChannel platform_views = MethodChannel('flutter/platform_views');
/// A [MethodChannel] for controlling platform views.
///
/// See also:
///
/// * [PlatformViewsService] for the available operations on this channel.
static const MethodChannel platform_views_2 = MethodChannel('flutter/platform_views_2');
/// A [MethodChannel] for configuring the Skia graphics library.
///
/// The following outgoing methods are defined for this channel (invoked using

View File

@ -1473,6 +1473,14 @@ class _AndroidViewSurfaceState extends State<AndroidViewSurface> {
@override
Widget build(BuildContext context) {
if (widget.controller.requiresViewComposition) {
if (widget.controller.useNewHybridComposition) {
// TODO(jonahwilliams): make it actually work.
return _PlatformLayerBasedAndroidViewSurface(
controller: widget.controller,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers,
);
}
return _PlatformLayerBasedAndroidViewSurface(
controller: widget.controller,
hitTestBehavior: widget.hitTestBehavior,

View File

@ -49,6 +49,7 @@ class FakeAndroidViewController implements AndroidViewController {
this.viewId, {
this.requiresSize = false,
this.requiresViewComposition = false,
this.useNewHybridComposition = false,
});
bool disposed = false;
@ -147,6 +148,9 @@ class FakeAndroidViewController implements AndroidViewController {
@override
bool requiresViewComposition;
@override
bool useNewHybridComposition;
}
class FakeAndroidPlatformViewsController {