diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 43c388b1e69f5..880d07fb97aa3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -771,29 +771,16 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, void FlutterPlatformViewsController::BringLayersIntoView(LayersMap layer_map) { FML_DCHECK(flutter_view_); UIView* flutter_view = flutter_view_.get(); - auto zIndex = 0; // Clear the `active_composition_order_`, which will be populated down below. active_composition_order_.clear(); for (size_t i = 0; i < composition_order_.size(); i++) { int64_t platform_view_id = composition_order_[i]; std::vector> layers = layer_map[platform_view_id]; UIView* platform_view_root = root_views_[platform_view_id].get(); - - if (platform_view_root.superview != flutter_view) { - [flutter_view addSubview:platform_view_root]; - } - // Make sure the platform_view_root is higher than the last platform_view_root in - // composition_order_. - platform_view_root.layer.zPosition = zIndex++; - + // `addSubview` will automatically reorder subview if it is already added. + [flutter_view addSubview:platform_view_root]; for (const std::shared_ptr& layer : layers) { - if ([layer->overlay_view_wrapper.get() superview] != flutter_view) { - [flutter_view addSubview:layer->overlay_view_wrapper]; - } - // Make sure all the overlays are higher than the platform view. - layer->overlay_view_wrapper.get().layer.zPosition = zIndex++; - FML_DCHECK(layer->overlay_view_wrapper.get().layer.zPosition > - platform_view_root.layer.zPosition); + [flutter_view addSubview:layer->overlay_view_wrapper]; } active_composition_order_.push_back(platform_view_id); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index bc65fca1d8c5e..2bd834f937a03 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -2345,6 +2345,101 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { XCTAssertEqual(flutterPlatformViewsController->GetCurrentBuilders().size(), 1UL); } +- (void)testFlutterPlatformViewControllerSubmitFrameShouldOrderSubviewsCorrectly { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @0, @"viewType" : @"MockFlutterPlatformView"}], + result); + UIView* view1 = gMockPlatformView; + + // This overwrites `gMockPlatformView` to another view. + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}], + result); + UIView* view2 = gMockPlatformView; + + flutterPlatformViewsController->BeginFrame(SkISize::Make(300, 300)); + flutter::MutatorsStack stack; + SkMatrix finalMatrix; + auto embeddedViewParams1 = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(0, std::move(embeddedViewParams1)); + flutterPlatformViewsController->CompositeEmbeddedView(0); + auto embeddedViewParams2 = + std::make_unique(finalMatrix, SkSize::Make(500, 500), stack); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams2)); + flutterPlatformViewsController->CompositeEmbeddedView(1); + + // SKSurface is required if the root FlutterView is present. + const SkImageInfo image_info = SkImageInfo::MakeN32Premul(1000, 1000); + sk_sp mock_sk_surface = SkSurface::MakeRaster(image_info); + flutter::SurfaceFrame::FramebufferInfo framebuffer_info; + auto mock_surface = std::make_unique( + std::move(mock_sk_surface), framebuffer_info, + [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }, + /*frame_size=*/SkISize::Make(800, 600)); + + XCTAssertTrue( + flutterPlatformViewsController->SubmitFrame(nullptr, nullptr, std::move(mock_surface))); + // platform view is wrapped by touch interceptor, which itself is wrapped by clipping view. + UIView* clippingView1 = view1.superview.superview; + UIView* clippingView2 = view2.superview.superview; + UIView* flutterView = clippingView1.superview; + XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] < + [flutterView.subviews indexOfObject:clippingView2], + @"The first clipping view should be added before the second clipping view."); + + // Need to recreate these params since they are `std::move`ed. + flutterPlatformViewsController->BeginFrame(SkISize::Make(300, 300)); + // Process the second frame in the opposite order. + embeddedViewParams2 = + std::make_unique(finalMatrix, SkSize::Make(500, 500), stack); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams2)); + flutterPlatformViewsController->CompositeEmbeddedView(1); + embeddedViewParams1 = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(0, std::move(embeddedViewParams1)); + flutterPlatformViewsController->CompositeEmbeddedView(0); + + mock_sk_surface = SkSurface::MakeRaster(image_info); + mock_surface = std::make_unique( + std::move(mock_sk_surface), framebuffer_info, + [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }, + /*frame_size=*/SkISize::Make(800, 600)); + XCTAssertTrue( + flutterPlatformViewsController->SubmitFrame(nullptr, nullptr, std::move(mock_surface))); + XCTAssertTrue([flutterView.subviews indexOfObject:clippingView1] > + [flutterView.subviews indexOfObject:clippingView2], + @"The first clipping view should be added after the second clipping view."); +} + - (void)testThreadMergeAtEndFrame { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner_platform = CreateNewThread("FlutterPlatformViewsTest1"); diff --git a/testing/ios/IosUnitTests/Tests/FlutterEngineConfig.xcconfig b/testing/ios/IosUnitTests/Tests/FlutterEngineConfig.xcconfig index fd5201bd71c5e..f54d553ef4f73 100644 --- a/testing/ios/IosUnitTests/Tests/FlutterEngineConfig.xcconfig +++ b/testing/ios/IosUnitTests/Tests/FlutterEngineConfig.xcconfig @@ -1,2 +1,3 @@ FLUTTER_ENGINE[arch=x86_64]=ios_debug_sim_unopt FLUTTER_ENGINE[arch=arm64]=ios_debug_sim_unopt_arm64 +FLUTTER_ENGINE=ios_debug_sim_unopt diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 2711d846bc631..28c84f8e6896f 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -60,6 +60,8 @@ - (BOOL)application:(UIApplication*)application @"--gesture-reject-after-touches-ended" : @"platform_view_gesture_reject_after_touches_ended", @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", @"--gesture-accept" : @"platform_view_gesture_accept", + @"--gesture-accept-with-overlapping-platform-views" : + @"platform_view_gesture_accept_with_overlapping_platform_views", @"--tap-status-bar" : @"tap_status_bar", @"--animated-color-square" : @"animated_color_square", @"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture", diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m index 0479b071de933..18bf5574063b7 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m @@ -165,4 +165,32 @@ - (XCUICoordinate*)getNormalizedCoordinate:(XCUIApplication*)app point:(CGVector return coordinate; } +- (void)testGestureWithOverlappingPlatformViews { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--gesture-accept-with-overlapping-platform-views" ]; + [app launch]; + + XCUIElement* foreground = app.otherElements[@"platform_view[0]"]; + XCTAssertEqual(foreground.frame.origin.x, 50); + XCTAssertEqual(foreground.frame.origin.y, 50); + XCTAssertEqual(foreground.frame.size.width, 50); + XCTAssertEqual(foreground.frame.size.height, 50); + XCTAssertTrue([foreground waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]); + + XCUIElement* background = app.otherElements[@"platform_view[1]"]; + XCTAssertEqual(background.frame.origin.x, 0); + XCTAssertEqual(background.frame.origin.y, 0); + XCTAssertEqual(background.frame.size.width, 150); + XCTAssertEqual(background.frame.size.height, 150); + XCTAssertTrue([background waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]); + + XCUIElement* textView = foreground.textViews.firstMatch; + XCTAssertTrue(textView.exists); + + XCTAssertTrue(foreground.isHittable); + [foreground tap]; + + XCTAssertEqualObjects(textView.label, + @"-gestureTouchesBegan-gestureTouchesEnded-platformViewTapped"); +} @end diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index adcb6dd73677b..3a6a29946bd4a 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -1056,6 +1056,133 @@ class PlatformViewForTouchIOSScenario extends Scenario } } +/// Scenario for verifying overlapping platform views can accept touch gesture. +/// See: https://github.com/flutter/flutter/issues/118366. +/// +/// Renders the first frame with a foreground platform view. +/// Then renders the second frame with the foreground platform view covering +/// a new background platform view. +/// +class PlatformViewForOverlappingPlatformViewsScenario extends Scenario + with _BasePlatformViewScenarioMixin { + + /// Creates the PlatformViewForOverlappingPlatformViewsScenario. + /// + /// The [dispatcher] parameter must not be null. + PlatformViewForOverlappingPlatformViewsScenario( + PlatformDispatcher dispatcher, { + required this.foregroundId, + required this.backgroundId, + }) : super(dispatcher) { + _nextFrame = _firstFrame; + } + + /// The id for a foreground platform view that covers another background platform view. + /// A good example is a dialog prompt in a real app. + final int foregroundId; + + /// The id for a background platform view that is covered by a foreground platform view. + final int backgroundId; + + late void Function() _nextFrame; + + @override + void onBeginFrame(Duration duration) { + _nextFrame(); + } + + void _firstFrame() { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(100, 100); + addPlatformView( + foregroundId, + width: 100, + height: 100, + dispatcher: dispatcher, + sceneBuilder: builder, + text: 'Foreground', + ); + builder.pop(); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } + + void _secondFrame() { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + addPlatformView( + backgroundId, + width: 300, + height: 300, + dispatcher: dispatcher, + sceneBuilder: builder, + text: 'Background', + ); + builder.pop(); + + builder.pushOffset(100, 100); + addPlatformView( + foregroundId, + width: 100, + height: 100, + dispatcher: dispatcher, + sceneBuilder: builder, + text: 'Foreground', + ); + builder.pop(); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } + + int _frameCount = 0; + + @override + void onDrawFrame() { + _frameCount += 1; + // TODO(hellohuanlin): Need further investigation - the first 2 frames are dropped for some reason. + // Wait for 60 frames to ensure the first frame has actually been rendered + // (Minimum required is 3 frames, but just to be safe) + if (_nextFrame == _firstFrame && _frameCount == 60) { + _nextFrame = _secondFrame; + } + window.scheduleFrame(); + super.onDrawFrame(); + } + + @override + void onPointerDataPacket(PointerDataPacket packet) { + final PointerData data = packet.data.first; + final double x = data.physicalX; + final double y = data.physicalY; + if (data.change == PointerChange.up && 100 <= x && x < 200 && 100 <= y && y < 200) { + const int valueString = 7; + const int valueInt32 = 3; + const int valueMap = 13; + final Uint8List message = Uint8List.fromList([ + valueString, + ..._encodeString('acceptGesture'), + valueMap, + 1, + valueString, + ..._encodeString('id'), + valueInt32, + ..._to32(foregroundId), + ]); + window.sendPlatformMessage( + 'flutter/platform_views', + message.buffer.asByteData(), + (ByteData? response) {}, + ); + } + } +} + /// A simple platform view for testing platform view with a continuous texture layer. /// For example, it simulates a video being played. class PlatformViewWithContinuousTexture extends PlatformViewScenario { diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index 94445d4254291..d07f933eee391 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -51,6 +51,7 @@ Map _scenarios = { 'platform_view_gesture_reject_eager': () => PlatformViewForTouchIOSScenario(PlatformDispatcher.instance, id: _viewId++, accept: false), 'platform_view_gesture_accept': () => PlatformViewForTouchIOSScenario(PlatformDispatcher.instance, id: _viewId++, accept: true), 'platform_view_gesture_reject_after_touches_ended': () => PlatformViewForTouchIOSScenario(PlatformDispatcher.instance, id: _viewId++, accept: false, rejectUntilTouchesEnded: true), + 'platform_view_gesture_accept_with_overlapping_platform_views': () => PlatformViewForOverlappingPlatformViewsScenario(PlatformDispatcher.instance, foregroundId: _viewId++, backgroundId: _viewId++), 'platform_view_scrolling_under_widget':()=>PlatformViewScrollingUnderWidget(PlatformDispatcher.instance, firstPlatformViewId: _viewId++, lastPlatformViewId: _viewId+=16), 'tap_status_bar': () => TouchesScenario(PlatformDispatcher.instance), 'initial_route_reply': () => InitialRouteReply(PlatformDispatcher.instance),