diff --git a/display_list/geometry/dl_rtree.cc b/display_list/geometry/dl_rtree.cc index 1b51a436e2a33..d3ae9a623d75f 100644 --- a/display_list/geometry/dl_rtree.cc +++ b/display_list/geometry/dl_rtree.cc @@ -201,18 +201,30 @@ void DlRTree::search(const Node& parent, } } -const DlRegion& DlRTree::region() const { +const DlRegion& DlRTree::region(bool useRoundIn) const { if (!region_) { std::vector rects; rects.resize(leaf_count_); for (int i = 0; i < leaf_count_; i++) { - nodes_[i].bounds.roundOut(&rects[i]); + if (useRoundIn) { + nodes_[i].bounds.roundIn(&rects[i]); + } else { + nodes_[i].bounds.roundOut(&rects[i]); + } } region_.emplace(rects); } return *region_; } +const DlRegion& DlRTree::region() const { + return region(/*useRoundIn=*/false); +} + +const DlRegion& DlRTree::roundedInRegion() const { + return region(/*useRoundIn=*/true); +} + const SkRect& DlRTree::bounds() const { if (!nodes_.empty()) { return nodes_.back().bounds; diff --git a/display_list/geometry/dl_rtree.h b/display_list/geometry/dl_rtree.h index a60e4e9d77c44..0b3f140e3d4f0 100644 --- a/display_list/geometry/dl_rtree.h +++ b/display_list/geometry/dl_rtree.h @@ -127,8 +127,11 @@ class DlRTree : public SkRefCnt { bool deband = true) const; /// Returns DlRegion that represents the union of all rectangles in the - /// R-Tree. + /// R-Tree. Each rectangle is rounded out. const DlRegion& region() const; + /// Returns DlRegion that represents the union of all rectangles in the + /// R-Tree. Each rectangle is rounded in. + const DlRegion& roundedInRegion() const; /// Returns DlRegion that represents the union of all rectangles in the /// R-Tree intersected with the query rect. @@ -147,6 +150,7 @@ class DlRTree : public SkRefCnt { int leaf_count_ = 0; int invalid_id_; mutable std::optional region_; + const DlRegion& region(bool useRoundIn) const; }; } // namespace flutter diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index 1907762ca0584..676880c8cd7de 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -26,6 +26,10 @@ const DlRegion& DisplayListEmbedderViewSlice::getRegion() const { return display_list_->rtree()->region(); } +const DlRegion& DisplayListEmbedderViewSlice::getRoundedInRegion() const { + return display_list_->rtree()->roundedInRegion(); +} + void DisplayListEmbedderViewSlice::render_into(DlCanvas* canvas) { canvas->DrawDisplayList(display_list_); } diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 5aad6605aaceb..d9ce4589bd435 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -338,7 +338,13 @@ class EmbedderViewSlice { virtual ~EmbedderViewSlice() = default; virtual DlCanvas* canvas() = 0; virtual void end_recording() = 0; + // TODO(hellohuanlin): deprecate `getRegion` and migrate to + // `getRoundedInRegion`. Then we should rename `getRoundedInRegion` to just + // `getRegion`. virtual const DlRegion& getRegion() const = 0; + // TODO(hellohuanlin): iOS only for now. Try on other platforms. + virtual const DlRegion& getRoundedInRegion() const = 0; + // TODO(hellohuanlin): We should deprecate this function if we migrate // all platforms to use `roundedInRegion`. Then we should rename // `roundedInRegion` to just `region`. @@ -355,13 +361,13 @@ class EmbedderViewSlice { // result in an intersection region of 1 px height, which is then used to // create an overlay layer. For each overlay, we acquire a surface frame, // paint the pixels and submit the frame. This resulted in performance - // issues since the surface frame acquisition is expensive. Since slice - // regions are already rounded out (see: - // https://github.com/flutter/engine/blob/5f40c9f49f88729bc3e71390356209dbe29ec788/display_list/geometry/dl_rtree.cc#L209), - // we can simply round in the queried rect to avoid the situation. - // After rounding in, it will ignore a single (or partial) pixel overlap, - // and give the ownership to the platform view. - return DlRegion::MakeIntersection(getRegion(), DlRegion(query.roundIn())); + // issues since the surface frame acquisition is expensive. + // We round in the layers and round out the platform view, rather than the + // opposite, so that the edge pixel overlay is guaranteed to be displayed + // on top of the platform view. After rounding in, layers will give the + // ownership of a single (or partial) pixel on the edge to platform views. + return DlRegion::MakeIntersection(getRoundedInRegion(), + DlRegion(query.roundOut())); } virtual void render_into(DlCanvas* canvas) = 0; @@ -375,6 +381,7 @@ class DisplayListEmbedderViewSlice : public EmbedderViewSlice { DlCanvas* canvas() override; void end_recording() override; const DlRegion& getRegion() const override; + const DlRegion& getRoundedInRegion() const override; void render_into(DlCanvas* canvas) override; void dispatch(DlOpReceiver& receiver); diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 4393b245135f6..47188144fcca7 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -52,7 +52,10 @@ - (BOOL)application:(UIApplication*)application @"platform_view_one_overlay_two_intersecting_overlays", @"--platform-view-multiple-without-overlays" : @"platform_view_multiple_without_overlays", @"--platform-view-max-overlays" : @"platform_view_max_overlays", - @"--platform-view-surrounding-layers" : @"platform_view_surrounding_layers", + @"--platform-view-surrounding-layers-fractional-coordinate" : + @"platform_view_surrounding_layers_fractional_coordinate", + @"--platform-view-partial-intersection-fractional-coordinate" : + @"platform_view_partial_intersection_fractional_coordinate", @"--platform-view-multiple" : @"platform_view_multiple", @"--platform-view-multiple-background-foreground" : @"platform_view_multiple_background_foreground", diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m index d1014e2186b00..e90487b7331de 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m @@ -313,9 +313,9 @@ - (void)testPlatformViewsMaxOverlays { // +---+----+---+ // | D | // +----+ -- (void)testPlatformViewsWithAdjacentSurroundingLayers { +- (void)testPlatformViewsWithAdjacentSurroundingLayersAndFractionalCoordinate { XCUIApplication* app = [[XCUIApplication alloc] init]; - app.launchArguments = @[ @"--platform-view-surrounding-layers" ]; + app.launchArguments = @[ @"--platform-view-surrounding-layers-fractional-coordinate" ]; [app launch]; XCUIElement* platform_view = app.otherElements[@"platform_view[0]"]; @@ -331,4 +331,36 @@ - (void)testPlatformViewsWithAdjacentSurroundingLayers { XCTAssertFalse(overlay.exists); } +// Platform view partially intersect with a layer in fractional coordinate. +// +-------+ +// | | +// | PV +--+--+ +// | | | +// +----+ A | +// | | +// +-----+ +- (void)testPlatformViewsWithPartialIntersectionAndFractionalCoordinate { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--platform-view-partial-intersection-fractional-coordinate" ]; + [app launch]; + + XCUIElement* platform_view = app.otherElements[@"platform_view[0]"]; + XCTAssertTrue([platform_view waitForExistenceWithTimeout:1.0]); + + CGFloat scale = [UIScreen mainScreen].scale; + XCTAssertEqual(platform_view.frame.origin.x * scale, 0.5); + XCTAssertEqual(platform_view.frame.origin.y * scale, 0.5); + XCTAssertEqual(platform_view.frame.size.width * scale, 100); + XCTAssertEqual(platform_view.frame.size.height * scale, 100); + + XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"]; + XCTAssert(overlay.exists); + + // 51 since layer is rounded in. + XCTAssertEqual(CGRectGetMinX(overlay.frame) * scale, 51); + XCTAssertEqual(CGRectGetMinY(overlay.frame) * scale, 51); + // 101 since platform view is rounded out. + XCTAssertEqual(CGRectGetMaxX(overlay.frame) * scale, 101); + XCTAssertEqual(CGRectGetMaxY(overlay.frame) * scale, 101); +} @end diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 799f53e36b894..096f4c73103c9 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -367,10 +367,10 @@ class PlatformViewMaxOverlaysScenario extends Scenario } /// A platform view with adjacent surrounding layers should not create overlays. -class PlatformViewSurroundingLayersScenario extends Scenario +class PlatformViewSurroundingLayersFractionalCoordinateScenario extends Scenario with _BasePlatformViewScenarioMixin { /// Creates the PlatformView scenario. - PlatformViewSurroundingLayersScenario( + PlatformViewSurroundingLayersFractionalCoordinateScenario( super.view, { required this.id, }); @@ -438,6 +438,55 @@ class PlatformViewSurroundingLayersScenario extends Scenario } } +/// A platform view partially intersect with a layer, both with fractional coordinates. +class PlatformViewPartialIntersectionFractionalCoordinateScenario extends Scenario + with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + PlatformViewPartialIntersectionFractionalCoordinateScenario( + super.view, { + required this.id, + }); + + /// The platform view identifier. + final int id; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + // Simulate partial pixel offsets as we would see while scrolling. + // All objects in the scene below are then on sub-pixel boundaries. + builder.pushOffset(0.5, 0.5); + + // a platform view from (0, 0) to (100, 100) + addPlatformView( + id, + width: 100, + height: 100, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + ); + + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + + canvas.drawRect( + Rect.fromLTWH(50, 50, 100, 100), + Paint()..color = const Color(0x22FF0000), + ); + + final Picture picture = recorder.endRecording(); + builder.addPicture(Offset.zero, picture); + + // Pop the (0.5, 0.5) offset. + builder.pop(); + + final Scene scene = builder.build(); + view.render(scene); + scene.dispose(); + } +} + /// Builds a scene with 2 platform views. class MultiPlatformViewScenario extends Scenario with _BasePlatformViewScenarioMixin { diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index d49693af719c4..bcd2c0cd908b6 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -34,7 +34,8 @@ Map _scenarios = { 'platform_view_one_overlay_two_intersecting_overlays': (FlutterView view) => PlatformViewOneOverlayTwoIntersectingOverlaysScenario(view, id: _viewId++), 'platform_view_multiple_without_overlays': (FlutterView view) => MultiPlatformViewWithoutOverlaysScenario(view, firstId: _viewId++, secondId: _viewId++), 'platform_view_max_overlays': (FlutterView view) => PlatformViewMaxOverlaysScenario(view, id: _viewId++), - 'platform_view_surrounding_layers': (FlutterView view) => PlatformViewSurroundingLayersScenario(view, id: _viewId++), + 'platform_view_surrounding_layers_fractional_coordinate': (FlutterView view) => PlatformViewSurroundingLayersFractionalCoordinateScenario(view, id: _viewId++), + 'platform_view_partial_intersection_fractional_coordinate': (FlutterView view) => PlatformViewPartialIntersectionFractionalCoordinateScenario(view, id: _viewId++), 'platform_view_cliprect': (FlutterView view) => PlatformViewClipRectScenario(view, id: _viewId++), 'platform_view_cliprect_with_transform': (FlutterView view) => PlatformViewClipRectWithTransformScenario(view, id: _viewId++), 'platform_view_cliprect_after_moved': (FlutterView view) => PlatformViewClipRectAfterMovedScenario(view, id: _viewId++),