diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 9a5387cf86b18..f04a444e41cfd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -277,7 +277,6 @@ // Eventually, the frame is submitted once this method returns `kSuccess`. // At that point, the raster tasks are handled on the platform thread. CancelFrame(); - raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration); return PostPrerollResult::kSkipAndRetryFrame; } // If the post preroll action is successful, we will display platform views in the current frame. @@ -289,6 +288,14 @@ return PostPrerollResult::kSuccess; } +void FlutterPlatformViewsController::EndFrame( + bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + if (should_resubmit_frame) { + raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration); + } +} + void FlutterPlatformViewsController::PrerollCompositeEmbeddedView( int view_id, std::unique_ptr params) { @@ -609,14 +616,18 @@ // This wrapper view masks the overlay view. overlay_view_wrapper.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale, rect.width() / screenScale, rect.height() / screenScale); - // Set a unique view identifier, so the overlay wrapper can be identified in unit tests. + // Set a unique view identifier, so the overlay_view_wrapper can be identified in XCUITests. overlay_view_wrapper.accessibilityIdentifier = [NSString stringWithFormat:@"platform_view[%lld].overlay[%lld]", view_id, overlay_id]; UIView* overlay_view = layer->overlay_view.get(); // Set the size of the overlay view. // This size is equal to the device screen size. - overlay_view.frame = flutter_view_.get().bounds; + overlay_view.frame = [flutter_view_.get() convertRect:flutter_view_.get().bounds + toView:overlay_view_wrapper]; + // Set a unique view identifier, so the overlay_view can be identified in XCUITests. + overlay_view.accessibilityIdentifier = + [NSString stringWithFormat:@"platform_view[%lld].overlay_view[%lld]", view_id, overlay_id]; std::unique_ptr frame = layer->surface->AcquireFrame(frame_size_); // If frame is null, AcquireFrame already printed out an error message. @@ -625,9 +636,6 @@ } SkCanvas* overlay_canvas = frame->SkiaCanvas(); overlay_canvas->clear(SK_ColorTRANSPARENT); - // Offset the picture since its absolute position on the scene is determined - // by the position of the overlay view. - overlay_canvas->translate(-rect.x(), -rect.y()); overlay_canvas->drawPicture(picture); layer->did_submit_last_frame = frame->Submit(); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index fba267419a116..08a109ace49fb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -979,11 +979,11 @@ - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashin std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams_2)); flutterPlatformViewsController->CompositeEmbeddedView(2); - auto mock_surface_submit_false = std::make_unique( + auto mock_surface_submit_true = std::make_unique( nullptr, framebuffer_info, [](const flutter::SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); XCTAssertTrue(flutterPlatformViewsController->SubmitFrame(nullptr, nullptr, - std::move(mock_surface_submit_false))); + std::move(mock_surface_submit_true))); } - (void) @@ -1088,6 +1088,67 @@ - (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder { XCTAssertEqual(flutterPlatformViewsController->GetCurrentCanvases().size(), 1UL); } +- (void)testThreadMergeAtEndFrame { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner_platform = CreateNewThread("FlutterPlatformViewsTest1"); + auto thread_task_runner_other = CreateNewThread("FlutterPlatformViewsTest2"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner_platform, + /*raster=*/thread_task_runner_other, + /*ui=*/thread_task_runner_other, + /*io=*/thread_task_runner_other); + 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); + XCTestExpectation* waitForPlatformView = + [self expectationWithDescription:@"wait for platform view to be created"]; + FlutterResult result = ^(id result) { + [waitForPlatformView fulfill]; + }; + + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + [self waitForExpectations:@[ waitForPlatformView ] timeout:30]; + XCTAssertNotNil(gMockPlatformView); + + flutterPlatformViewsController->BeginFrame(SkISize::Make(300, 300)); + SkMatrix finalMatrix; + flutter::MutatorsStack stack; + auto embeddedViewParams = + std::make_unique(finalMatrix, SkSize::Make(300, 300), stack); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + + fml::RefPtr raster_thread_merger = + fml::MakeRefCounted(thread_task_runner_platform->GetTaskQueueId(), + thread_task_runner_other->GetTaskQueueId()); + XCTAssertEqual(flutterPlatformViewsController->PostPrerollAction(raster_thread_merger), + flutter::PostPrerollResult::kSkipAndRetryFrame); + XCTAssertFalse(raster_thread_merger->IsMerged()); + + flutterPlatformViewsController->EndFrame(true, raster_thread_merger); + XCTAssertTrue(raster_thread_merger->IsMerged()); + + // Unmerge threads before the end of the test + // TaskRunners are required to be unmerged before destruction. + while (raster_thread_merger->DecrementLease() != fml::RasterThreadStatus::kUnmergedNow) + ; +} + - (int)alphaOfPoint:(CGPoint)point onView:(UIView*)view { unsigned char pixel[4] = {0}; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 8d42bc879488d..c20f957633954 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -160,6 +160,9 @@ class FlutterPlatformViewsController { PostPrerollResult PostPrerollAction(fml::RefPtr raster_thread_merger); + void EndFrame(bool should_resubmit_frame, + fml::RefPtr raster_thread_merger); + std::vector GetCurrentCanvases(); SkCanvas* CompositeEmbeddedView(int view_id); diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.mm b/shell/platform/darwin/ios/ios_external_view_embedder.mm index a4921af254af9..1323fa4c2f487 100644 --- a/shell/platform/darwin/ios/ios_external_view_embedder.mm +++ b/shell/platform/darwin/ios/ios_external_view_embedder.mm @@ -84,7 +84,7 @@ void IOSExternalViewEmbedder::EndFrame(bool should_resubmit_frame, fml::RefPtr raster_thread_merger) { TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::EndFrame"); - FML_CHECK(platform_views_controller_); + platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger); } // |ExternalViewEmbedder| diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m index ac301f3922b3b..51ab28fadeaae 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m @@ -4,6 +4,8 @@ #import +static const CGFloat kCompareAccuracy = 0.001; + @interface UnobstructedPlatformViewTests : XCTestCase @end @@ -60,6 +62,15 @@ - (void)testOneOverlay { XCTAssertEqual(overlay.frame.origin.y, 150); XCTAssertEqual(overlay.frame.size.width, 50); XCTAssertEqual(overlay.frame.size.height, 50); + + XCUIElement* overlayView = app.otherElements[@"platform_view[0].overlay_view[0]"]; + XCTAssertTrue(overlayView.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView.frame.size.height, app.frame.size.height, + kCompareAccuracy); } // A is the layer above the platform view. @@ -86,6 +97,15 @@ - (void)testOneOverlayPartialIntersection { XCTAssertEqual(overlay.frame.size.width, 50); // Half the height of the overlay. XCTAssertEqual(overlay.frame.size.height, 25); + + XCUIElement* overlayView = app.otherElements[@"platform_view[0].overlay_view[0]"]; + XCTAssertTrue(overlayView.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView.frame.size.height, app.frame.size.height, + kCompareAccuracy); } // A and B are the layers above the platform view. @@ -149,6 +169,24 @@ - (void)testOneOverlayAndTwoIntersectingOverlays { XCTAssertEqual(overlay2.frame.origin.y, 225); XCTAssertEqual(overlay2.frame.size.width, 50); XCTAssertEqual(overlay2.frame.size.height, 50); + + XCUIElement* overlayView0 = app.otherElements[@"platform_view[0].overlay_view[0]"]; + XCTAssertTrue(overlayView0.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView0.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.size.height, app.frame.size.height, + kCompareAccuracy); + + XCUIElement* overlayView1 = app.otherElements[@"platform_view[0].overlay_view[0]"]; + XCTAssertTrue(overlayView1.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView1.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView1.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView1.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView1.frame.size.height, app.frame.size.height, + kCompareAccuracy); } // A is the layer, which z index is higher than the platform view. @@ -179,6 +217,8 @@ - (void)testMultiplePlatformViewsWithoutOverlays { XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[0]"].exists); XCTAssertFalse(app.otherElements[@"platform_view[1].overlay[0]"].exists); + XCTAssertFalse(app.otherElements[@"platform_view[0].overlay_view[0]"].exists); + XCTAssertFalse(app.otherElements[@"platform_view[1].overlay_view[0]"].exists); } // A is the layer above both platform view. @@ -220,6 +260,24 @@ - (void)testMultiplePlatformViewsWithOverlays { XCTAssertEqual(overlay2.frame.origin.y, 25); XCTAssertEqual(overlay2.frame.size.width, 200); XCTAssertEqual(overlay2.frame.size.height, 250); + + XCUIElement* overlayView0 = app.otherElements[@"platform_view[0].overlay_view[0]"]; + XCTAssertTrue(overlayView0.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView0.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.size.height, app.frame.size.height, + kCompareAccuracy); + + XCUIElement* overlayView1 = app.otherElements[@"platform_view[1].overlay_view[0]"]; + XCTAssertTrue(overlayView1.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView1.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView1.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView1.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView1.frame.size.height, app.frame.size.height, + kCompareAccuracy); } // More then two overlays are merged into a single layer. @@ -246,6 +304,18 @@ - (void)testPlatformViewsMaxOverlays { XCTAssertTrue(overlay.exists); XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[1]"].exists); XCTAssertTrue(CGRectContainsRect(platform_view.frame, overlay.frame)); + + XCUIElement* overlayView0 = app.otherElements[@"platform_view[0].overlay_view[0]"]; + XCTAssertTrue(overlayView0.exists); + // Overlay should always be the same frame as the app. + XCTAssertEqualWithAccuracy(overlayView0.frame.origin.x, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.origin.y, app.frame.origin.x, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.size.width, app.frame.size.width, kCompareAccuracy); + XCTAssertEqualWithAccuracy(overlayView0.frame.size.height, app.frame.size.height, + kCompareAccuracy); + + XCUIElement* overlayView1 = app.otherElements[@"platform_view[0].overlay_view[1]"]; + XCTAssertFalse(overlayView1.exists); } @end