From 761c1623ca6b1b179713eaf472f491d1396e63b1 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 11 Feb 2025 09:13:19 -0800 Subject: [PATCH] [Android] fix hcpp overlay layer intersection. (#163024) Since we only have a single overlay layer, we need to diff out the platform views that _would_ intersect if we did the correct layering. --- .../hcpp/platform_view_overlapping_main.dart | 125 ++++++++++++++++++ .../platform_view_overlapping_main_test.dart | 61 +++++++++ .../external_view_embedder_2.cc | 20 ++- 3 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 dev/integration_tests/android_engine_test/lib/hcpp/platform_view_overlapping_main.dart create mode 100644 dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_overlapping_main_test.dart diff --git a/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_overlapping_main.dart b/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_overlapping_main.dart new file mode 100644 index 0000000000..e38f7302a4 --- /dev/null +++ b/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_overlapping_main.dart @@ -0,0 +1,125 @@ +// 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 'dart:convert'; + +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( + handler: (String? command) async { + return json.encode({ + 'supported': await HybridAndroidViewController.checkIfSupported(), + }); + }, + commands: [nativeDriverCommands], + ); + + // Run on full screen. + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + runApp(const MainApp()); +} + +// This should appear as the yellow line over a blue box. The +// green box should not be visible unless the platform view has not loaded yet. +final class MainApp extends StatefulWidget { + const MainApp({super.key}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Stack( + children: [ + Positioned.directional( + top: 100, + textDirection: TextDirection.ltr, + child: const SizedBox( + width: 200, + height: 200, + child: _HybridCompositionAndroidPlatformView(viewType: 'box_platform_view'), + ), + ), + Positioned.directional( + top: 200, + textDirection: TextDirection.ltr, + child: const SizedBox(width: 800, height: 200, child: ColoredBox(color: Colors.yellow)), + ), + Positioned.directional( + top: 300, + textDirection: TextDirection.ltr, + child: const SizedBox( + width: 200, + height: 200, + child: _HybridCompositionAndroidPlatformView(viewType: 'box_platform_view'), + ), + ), + Positioned.directional( + top: 400, + textDirection: TextDirection.ltr, + child: const SizedBox(width: 800, height: 200, child: ColoredBox(color: Colors.red)), + ), + Positioned.directional( + top: 500, + textDirection: TextDirection.ltr, + child: const SizedBox( + width: 200, + height: 200, + child: _HybridCompositionAndroidPlatformView(viewType: 'box_platform_view'), + ), + ), + Positioned.directional( + top: 600, + textDirection: TextDirection.ltr, + child: const SizedBox(width: 800, height: 200, child: ColoredBox(color: Colors.orange)), + ), + ], + ), + ); + } +} + +final class _HybridCompositionAndroidPlatformView extends StatelessWidget { + const _HybridCompositionAndroidPlatformView({required this.viewType}); + + final String viewType; + + @override + Widget build(BuildContext context) { + return PlatformViewLink( + viewType: viewType, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initHybridAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + } +} diff --git a/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_overlapping_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_overlapping_main_test.dart new file mode 100644 index 0000000000..1a53cc71b4 --- /dev/null +++ b/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_overlapping_main_test.dart @@ -0,0 +1,61 @@ +// 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 'dart:convert'; + +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_overlapping_main.dart +/// +/// # Make your changes. +/// +/// # Run the test against baseline. +/// flutter drive lib/platform_view/hcpp/platform_view_overlapping_main.dart +/// ``` +/// +/// For a convenient way to deflake a test, see `tool/deflake.dart`. +void main() async { + const String goldenPrefix = 'hybrid_composition_pp_overlapping_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('verify that HCPP is supported and enabled', () async { + final Map response = + json.decode(await flutterDriver.requestData('')) as Map; + + expect(response['supported'], true); + }, timeout: Timeout.none); + + test('should screenshot multiple HCPP platform view with overlays', () async { + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.multiple_overlays.png'), + ); + }, timeout: Timeout.none); +} diff --git a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc index d233e6d76b..3420b755b7 100644 --- a/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc +++ b/engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc @@ -100,7 +100,6 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView( // 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 latch = std::make_shared(1u); @@ -112,12 +111,14 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView( })); latch->Wait(); } + surface_pool_->ResetLayers(); // Create Overlay frame. If overlay surface creation failed, // all this work must be skipped. std::unique_ptr overlay_frame; if (surface_pool_->HasLayers()) { - for (int64_t view_id : composition_order_) { + for (size_t i = 0; i < composition_order_.size(); i++) { + int64_t view_id = composition_order_[i]; std::unordered_map::const_iterator overlay = overlay_layers.find(view_id); @@ -128,13 +129,26 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView( std::shared_ptr layer = surface_pool_->GetLayer( context, android_context_, jni_facade_, surface_factory_); overlay_frame = layer->surface->AcquireFrame(frame_size_); + overlay_frame->Canvas()->Clear(flutter::DlColor::kTransparent()); } DlCanvas* overlay_canvas = overlay_frame->Canvas(); int restore_count = overlay_canvas->GetSaveCount(); overlay_canvas->Save(); overlay_canvas->ClipRect(overlay->second); - overlay_canvas->Clear(DlColor::kTransparent()); + + // For all following platform views that would cover this overlay, + // emulate the effect by adding a difference clip. This makes the + // overlays appear as if they are under the platform view, when in + // reality there is only a single layer. + for (size_t j = i + 1; j < composition_order_.size(); j++) { + SkRect view_rect = GetViewRect(composition_order_[j], view_params_); + overlay_canvas->ClipRect( + DlRect::MakeLTRB(view_rect.left(), view_rect.top(), + view_rect.right(), view_rect.bottom()), + DlClipOp::kDifference); + } + slices_[view_id]->render_into(overlay_canvas); overlay_canvas->RestoreToCount(restore_count); }