Reland "[ios][platform_view] Use CAShapeLayer as the mask to avoid software rendering #53072" (flutter/engine#53256)

The previous PR was reverted because it was on top of an outdated commit prior to ARC. This PR rebases the main. The only change is removing the `[super dealloc]` call that caused the compiler error. 

*List which issues are fixed by this PR. You must list at least one issue.*
Fixes https://github.com/flutter/flutter/issues/142813
Fixes https://github.com/flutter/flutter/issues/142830

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
hellohuanlin 2024-06-06 15:33:18 -07:00 committed by GitHub
parent 21b03cd398
commit 6ea5724ca1
13 changed files with 149 additions and 38 deletions

View File

@ -1773,17 +1773,15 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
[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);
}
}
}
@ -1848,17 +1846,42 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
[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);
}
}
}
@ -1924,17 +1947,42 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
[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);
}
}
}
@ -2989,6 +3037,69 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(const std::string& name) {
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<flutter::FlutterPlatformViewsController>();
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*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<fml::SyncSwitch>());
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<flutter::EmbeddedViewParams>(
screenScaleMatrix, SkSize::Make(10, 10), stack1);
flutter::MutatorsStack stack2;
stack2.PushClipRect(rect);
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
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.
//

View File

@ -236,12 +236,12 @@ static BOOL _preparedOnce = NO;
// information about screen scale.
@property(nonatomic) CATransform3D reverseScreenScale;
- (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
- (void)addTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
@end
@implementation FlutterClippingMaskView {
std::vector<fml::CFRef<CGPathRef>> paths_;
CGMutablePathRef pathSoFar_;
}
- (instancetype)initWithFrame:(CGRect)frame {
@ -252,15 +252,30 @@ static BOOL _preparedOnce = NO;
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_);
}
// 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.
@ -270,28 +285,13 @@ static BOOL _preparedOnce = NO;
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 {
@ -360,7 +360,7 @@ static BOOL _preparedOnce = NO;
// 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 {
@ -425,15 +425,15 @@ static BOOL _preparedOnce = NO;
// 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<CGPathRef>)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<CGPathRef>(transformedPath);
}
@end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 41 KiB