[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:
parent
7569fbfce5
commit
f0396970e9
@ -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" />';
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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),
|
||||
|
@ -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();
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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_;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user