diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 2dc397d756a7c..58c924c7253d7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -1741,17 +1741,15 @@ - (void)testClipRect { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; + CGRect insideClipping = CGRectMake(2, 2, 3, 3); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { CGPoint point = CGPointMake(i, j); int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; - // Edges of the clipping might have a semi transparent pixel, we only check the pixels that - // are fully inside the clipped area. - CGRect insideClipping = CGRectMake(3, 3, 1, 1); if (CGRectContainsPoint(insideClipping, point)) { XCTAssertEqual(alpha, 255); } else { - XCTAssertLessThan(alpha, 255); + XCTAssertEqual(alpha, 0); } } } @@ -1816,17 +1814,42 @@ - (void)testClipRRect { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; + /* + ClippingMask outterClipping + 2 3 4 5 6 7 2 3 4 5 6 7 + 2 / - - - - \ 2 + - - - - + + 3 | | 3 | | + 4 | | 4 | | + 5 | | 5 | | + 6 | | 6 | | + 7 \ - - - - / 7 + - - - - + + + innerClipping1 innerClipping2 + 2 3 4 5 6 7 2 3 4 5 6 7 + 2 + - - + 2 + 3 | | 3 + - - - - + + 4 | | 4 | | + 5 | | 5 | | + 6 | | 6 + - - - - + + 7 + - - + 7 + */ + CGRect innerClipping1 = CGRectMake(3, 2, 4, 6); + CGRect innerClipping2 = CGRectMake(2, 3, 6, 4); + CGRect outterClipping = CGRectMake(2, 2, 6, 6); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { CGPoint point = CGPointMake(i, j); int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; - // Edges of the clipping might have a semi transparent pixel, we only check the pixels that - // are fully inside the clipped area. - CGRect insideClipping = CGRectMake(3, 3, 4, 4); - if (CGRectContainsPoint(insideClipping, point)) { + if (CGRectContainsPoint(innerClipping1, point) || + CGRectContainsPoint(innerClipping2, point)) { + // Pixels inside either of the 2 inner clippings should be fully opaque. XCTAssertEqual(alpha, 255); + } else if (CGRectContainsPoint(outterClipping, point)) { + // Corner pixels (i.e. (2, 2), (2, 7), (7, 2) and (7, 7)) should be partially transparent. + XCTAssert(0 < alpha && alpha < 255); } else { - XCTAssertLessThan(alpha, 255); + // Pixels outside outterClipping should be fully transparent. + XCTAssertEqual(alpha, 0); } } } @@ -1892,17 +1915,42 @@ - (void)testClipPath { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; + /* + ClippingMask outterClipping + 2 3 4 5 6 7 2 3 4 5 6 7 + 2 / - - - - \ 2 + - - - - + + 3 | | 3 | | + 4 | | 4 | | + 5 | | 5 | | + 6 | | 6 | | + 7 \ - - - - / 7 + - - - - + + + innerClipping1 innerClipping2 + 2 3 4 5 6 7 2 3 4 5 6 7 + 2 + - - + 2 + 3 | | 3 + - - - - + + 4 | | 4 | | + 5 | | 5 | | + 6 | | 6 + - - - - + + 7 + - - + 7 + */ + CGRect innerClipping1 = CGRectMake(3, 2, 4, 6); + CGRect innerClipping2 = CGRectMake(2, 3, 6, 4); + CGRect outterClipping = CGRectMake(2, 2, 6, 6); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { CGPoint point = CGPointMake(i, j); int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView]; - // Edges of the clipping might have a semi transparent pixel, we only check the pixels that - // are fully inside the clipped area. - CGRect insideClipping = CGRectMake(3, 3, 4, 4); - if (CGRectContainsPoint(insideClipping, point)) { + if (CGRectContainsPoint(innerClipping1, point) || + CGRectContainsPoint(innerClipping2, point)) { + // Pixels inside either of the 2 inner clippings should be fully opaque. XCTAssertEqual(alpha, 255); + } else if (CGRectContainsPoint(outterClipping, point)) { + // Corner pixels (i.e. (2, 2), (2, 7), (7, 2) and (7, 7)) should be partially transparent. + XCTAssert(0 < alpha && alpha < 255); } else { - XCTAssertLessThan(alpha, 255); + // Pixels outside outterClipping should be fully transparent. + XCTAssertEqual(alpha, 0); } } } @@ -2957,6 +3005,69 @@ - (void)testDifferentClipMaskViewIsUsedForEachView { XCTAssertNotEqual(maskView1, maskView2); } +- (void)testMaskViewUsesCAShapeLayerAsTheBackingLayer { + 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=*/mock_delegate.settings_.enable_impeller + ? flutter::IOSRenderingAPI::kMetal + : flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_jsync_switch=*/std::make_shared()); + + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = + [[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init]; + flutterPlatformViewsController->RegisterViewFactory( + factory, @"MockFlutterPlatformView", + FlutterPlatformViewGestureRecognizersBlockingPolicyEager); + FlutterResult result = ^(id result) { + }; + + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}], + result); + + XCTAssertNotNil(gMockPlatformView); + UIView* mockFlutterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack1; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack1.PushTransform(screenScaleMatrix); + // Push a clip rect + SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3); + stack1.PushClipRect(rect); + + auto embeddedViewParams1 = std::make_unique( + screenScaleMatrix, SkSize::Make(10, 10), stack1); + + flutter::MutatorsStack stack2; + stack2.PushClipRect(rect); + auto embeddedViewParams2 = std::make_unique( + screenScaleMatrix, SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1)); + flutterPlatformViewsController->CompositeEmbeddedView(1); + UIView* childClippingView = gMockPlatformView.superview.superview; + + UIView* maskView = childClippingView.maskView; + XCTAssert([maskView.layer isKindOfClass:[CAShapeLayer class]], + @"Mask view must use CAShapeLayer as its backing layer."); +} + // Return true if a correct visual effect view is found. It also implies all the validation in this // method passes. // diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 2aa13f5ae09f5..94f7ea95ed923 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -255,12 +255,12 @@ @interface FlutterClippingMaskView () // information about screen scale. @property(nonatomic) CATransform3D reverseScreenScale; -- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix; +- (void)addTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix; @end @implementation FlutterClippingMaskView { - std::vector> paths_; + CGMutablePathRef pathSoFar_; } - (instancetype)initWithFrame:(CGRect)frame { @@ -271,15 +271,31 @@ - (instancetype)initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale { if (self = [super initWithFrame:frame]) { self.backgroundColor = UIColor.clearColor; _reverseScreenScale = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1); + pathSoFar_ = CGPathCreateMutable(); } return self; } ++ (Class)layerClass { + return [CAShapeLayer class]; +} + +- (CAShapeLayer*)shapeLayer { + return (CAShapeLayer*)self.layer; +} + - (void)reset { - paths_.clear(); + CGPathRelease(pathSoFar_); + pathSoFar_ = CGPathCreateMutable(); + [self shapeLayer].path = nil; [self setNeedsDisplay]; } +- (void)dealloc { + CGPathRelease(pathSoFar_); + [super dealloc]; +} + // In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added // this view as a subview of the ChildClippingView. // This results this view blocking touch events on the ChildClippingView. @@ -289,28 +305,13 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } -- (void)drawRect:(CGRect)rect { - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSaveGState(context); - - // For mask view, only the alpha channel is used. - CGContextSetAlpha(context, 1); - - for (size_t i = 0; i < paths_.size(); i++) { - CGContextAddPath(context, paths_.at(i)); - CGContextClip(context); - } - CGContextFillRect(context, rect); - CGContextRestoreGState(context); -} - - (void)clipRect:(const SkRect&)clipSkRect matrix:(const SkMatrix&)matrix { CGRect clipRect = flutter::GetCGRectFromSkRect(clipSkRect); CGPathRef path = CGPathCreateWithRect(clipRect, nil); // The `matrix` is based on the physical pixels, convert it to UIKit points. CATransform3D matrixInPoints = CATransform3DConcat(flutter::GetCATransform3DFromSkMatrix(matrix), _reverseScreenScale); - paths_.push_back([self getTransformedPath:path matrix:matrixInPoints]); + [self addTransformedPath:path matrix:matrixInPoints]; } - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const SkMatrix&)matrix { @@ -379,7 +380,7 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const SkMatrix&)matrix { // TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that // the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge // clipping on iOS. - paths_.push_back([self getTransformedPath:pathRef matrix:matrixInPoints]); + [self addTransformedPath:pathRef matrix:matrixInPoints]; } - (void)clipPath:(const SkPath&)path matrix:(const SkMatrix&)matrix { @@ -444,15 +445,15 @@ - (void)clipPath:(const SkPath&)path matrix:(const SkMatrix&)matrix { // The `matrix` is based on the physical pixels, convert it to UIKit points. CATransform3D matrixInPoints = CATransform3DConcat(flutter::GetCATransform3DFromSkMatrix(matrix), _reverseScreenScale); - paths_.push_back([self getTransformedPath:pathRef matrix:matrixInPoints]); + [self addTransformedPath:pathRef matrix:matrixInPoints]; } -- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { +- (void)addTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { CGAffineTransform affine = CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); - CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine); + CGPathAddPath(pathSoFar_, &affine, path); + [self shapeLayer].path = pathSoFar_; CGPathRelease(path); - return fml::CFRef(transformedPath); } @end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE (3rd generation)_17.0_simulator.png index 2bebde1df95ce..7ea1bc4170523 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_iPhone SE (3rd generation)_17.0_simulator.png index 8b6157a9ee92b..02d912869b2a4 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_iPhone SE (3rd generation)_17.0_simulator.png index f550dafa6a86d..8d206240351da 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png index 9204afaccbd1a..80d7b7da3739c 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png index 864e2e4cc6312..bbdf631e40b64 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png index f099ea658da10..3402fa4176526 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png index e7b071aa7b4e0..f91b63d8c75e1 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png index 46af77aaf34a7..177d0c817fdb0 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_17.0_simulator.png index cb1d6c12423cb..f93545135c812 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_17.0_simulator.png index fca605741bb17..49f7cadbbfece 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_17.0_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_17.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_17.0_simulator.png index 0d8f3cc70cb97..56ba9c5979563 100644 Binary files a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_17.0_simulator.png and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_17.0_simulator.png differ