Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 1314faf

Browse files
authored
[ios][platform_view] Use CAShapeLayer as the mask to avoid software rendering (#53072)
This PR uses `CAShapeLayer` as the mask to avoid software rendering. I kept `UIView` as the mask, so that we can measure just the improvement related to avoiding software rendering. This also allows me to land this change sooner. I created [a separate issue](flutter/flutter#149212) to track removing UIView as the mask. Note: the previous behavior seems to be incorrect (or at least not pixel perfect). This PR fixed it. See comments. See design doc: https://docs.google.com/document/d/1TqG_N4GK_qctuk73Gk3zOdAiILUrwMqxoCMgroK_AeA/edit?resourcekey=0-jUiidfzIS642ngG2w9vSUA&tab=t.0 *List which issues are fixed by this PR. You must list at least one issue.* Fixes flutter/flutter#142813 Fixes flutter/flutter#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
1 parent 43c3d32 commit 1314faf

13 files changed

+150
-38
lines changed

shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm

Lines changed: 125 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,17 +1773,15 @@ - (void)testClipRect {
17731773
[mockFlutterView setNeedsLayout];
17741774
[mockFlutterView layoutIfNeeded];
17751775

1776+
CGRect insideClipping = CGRectMake(2, 2, 3, 3);
17761777
for (int i = 0; i < 10; i++) {
17771778
for (int j = 0; j < 10; j++) {
17781779
CGPoint point = CGPointMake(i, j);
17791780
int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView];
1780-
// Edges of the clipping might have a semi transparent pixel, we only check the pixels that
1781-
// are fully inside the clipped area.
1782-
CGRect insideClipping = CGRectMake(3, 3, 1, 1);
17831781
if (CGRectContainsPoint(insideClipping, point)) {
17841782
XCTAssertEqual(alpha, 255);
17851783
} else {
1786-
XCTAssertLessThan(alpha, 255);
1784+
XCTAssertEqual(alpha, 0);
17871785
}
17881786
}
17891787
}
@@ -1848,17 +1846,42 @@ - (void)testClipRRect {
18481846
[mockFlutterView setNeedsLayout];
18491847
[mockFlutterView layoutIfNeeded];
18501848

1849+
/*
1850+
ClippingMask outterClipping
1851+
2 3 4 5 6 7 2 3 4 5 6 7
1852+
2 / - - - - \ 2 + - - - - +
1853+
3 | | 3 | |
1854+
4 | | 4 | |
1855+
5 | | 5 | |
1856+
6 | | 6 | |
1857+
7 \ - - - - / 7 + - - - - +
1858+
1859+
innerClipping1 innerClipping2
1860+
2 3 4 5 6 7 2 3 4 5 6 7
1861+
2 + - - + 2
1862+
3 | | 3 + - - - - +
1863+
4 | | 4 | |
1864+
5 | | 5 | |
1865+
6 | | 6 + - - - - +
1866+
7 + - - + 7
1867+
*/
1868+
CGRect innerClipping1 = CGRectMake(3, 2, 4, 6);
1869+
CGRect innerClipping2 = CGRectMake(2, 3, 6, 4);
1870+
CGRect outterClipping = CGRectMake(2, 2, 6, 6);
18511871
for (int i = 0; i < 10; i++) {
18521872
for (int j = 0; j < 10; j++) {
18531873
CGPoint point = CGPointMake(i, j);
18541874
int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView];
1855-
// Edges of the clipping might have a semi transparent pixel, we only check the pixels that
1856-
// are fully inside the clipped area.
1857-
CGRect insideClipping = CGRectMake(3, 3, 4, 4);
1858-
if (CGRectContainsPoint(insideClipping, point)) {
1875+
if (CGRectContainsPoint(innerClipping1, point) ||
1876+
CGRectContainsPoint(innerClipping2, point)) {
1877+
// Pixels inside either of the 2 inner clippings should be fully opaque.
18591878
XCTAssertEqual(alpha, 255);
1879+
} else if (CGRectContainsPoint(outterClipping, point)) {
1880+
// Corner pixels (i.e. (2, 2), (2, 7), (7, 2) and (7, 7)) should be partially transparent.
1881+
XCTAssert(0 < alpha && alpha < 255);
18601882
} else {
1861-
XCTAssertLessThan(alpha, 255);
1883+
// Pixels outside outterClipping should be fully transparent.
1884+
XCTAssertEqual(alpha, 0);
18621885
}
18631886
}
18641887
}
@@ -1924,17 +1947,42 @@ - (void)testClipPath {
19241947
[mockFlutterView setNeedsLayout];
19251948
[mockFlutterView layoutIfNeeded];
19261949

1950+
/*
1951+
ClippingMask outterClipping
1952+
2 3 4 5 6 7 2 3 4 5 6 7
1953+
2 / - - - - \ 2 + - - - - +
1954+
3 | | 3 | |
1955+
4 | | 4 | |
1956+
5 | | 5 | |
1957+
6 | | 6 | |
1958+
7 \ - - - - / 7 + - - - - +
1959+
1960+
innerClipping1 innerClipping2
1961+
2 3 4 5 6 7 2 3 4 5 6 7
1962+
2 + - - + 2
1963+
3 | | 3 + - - - - +
1964+
4 | | 4 | |
1965+
5 | | 5 | |
1966+
6 | | 6 + - - - - +
1967+
7 + - - + 7
1968+
*/
1969+
CGRect innerClipping1 = CGRectMake(3, 2, 4, 6);
1970+
CGRect innerClipping2 = CGRectMake(2, 3, 6, 4);
1971+
CGRect outterClipping = CGRectMake(2, 2, 6, 6);
19271972
for (int i = 0; i < 10; i++) {
19281973
for (int j = 0; j < 10; j++) {
19291974
CGPoint point = CGPointMake(i, j);
19301975
int alpha = [self alphaOfPoint:CGPointMake(i, j) onView:mockFlutterView];
1931-
// Edges of the clipping might have a semi transparent pixel, we only check the pixels that
1932-
// are fully inside the clipped area.
1933-
CGRect insideClipping = CGRectMake(3, 3, 4, 4);
1934-
if (CGRectContainsPoint(insideClipping, point)) {
1976+
if (CGRectContainsPoint(innerClipping1, point) ||
1977+
CGRectContainsPoint(innerClipping2, point)) {
1978+
// Pixels inside either of the 2 inner clippings should be fully opaque.
19351979
XCTAssertEqual(alpha, 255);
1980+
} else if (CGRectContainsPoint(outterClipping, point)) {
1981+
// Corner pixels (i.e. (2, 2), (2, 7), (7, 2) and (7, 7)) should be partially transparent.
1982+
XCTAssert(0 < alpha && alpha < 255);
19361983
} else {
1937-
XCTAssertLessThan(alpha, 255);
1984+
// Pixels outside outterClipping should be fully transparent.
1985+
XCTAssertEqual(alpha, 0);
19381986
}
19391987
}
19401988
}
@@ -2989,6 +3037,69 @@ - (void)testDifferentClipMaskViewIsUsedForEachView {
29893037
XCTAssertNotEqual(maskView1, maskView2);
29903038
}
29913039

3040+
- (void)testMaskViewUsesCAShapeLayerAsTheBackingLayer {
3041+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
3042+
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
3043+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
3044+
/*platform=*/thread_task_runner,
3045+
/*raster=*/thread_task_runner,
3046+
/*ui=*/thread_task_runner,
3047+
/*io=*/thread_task_runner);
3048+
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
3049+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
3050+
/*delegate=*/mock_delegate,
3051+
/*rendering_api=*/mock_delegate.settings_.enable_impeller
3052+
? flutter::IOSRenderingAPI::kMetal
3053+
: flutter::IOSRenderingAPI::kSoftware,
3054+
/*platform_views_controller=*/flutterPlatformViewsController,
3055+
/*task_runners=*/runners,
3056+
/*worker_task_runner=*/nil,
3057+
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
3058+
3059+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
3060+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
3061+
flutterPlatformViewsController->RegisterViewFactory(
3062+
factory, @"MockFlutterPlatformView",
3063+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
3064+
FlutterResult result = ^(id result) {
3065+
};
3066+
3067+
flutterPlatformViewsController->OnMethodCall(
3068+
[FlutterMethodCall
3069+
methodCallWithMethodName:@"create"
3070+
arguments:@{@"id" : @1, @"viewType" : @"MockFlutterPlatformView"}],
3071+
result);
3072+
3073+
XCTAssertNotNil(gMockPlatformView);
3074+
UIView* mockFlutterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
3075+
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
3076+
// Create embedded view params
3077+
flutter::MutatorsStack stack1;
3078+
// Layer tree always pushes a screen scale factor to the stack
3079+
SkMatrix screenScaleMatrix =
3080+
SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
3081+
stack1.PushTransform(screenScaleMatrix);
3082+
// Push a clip rect
3083+
SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3);
3084+
stack1.PushClipRect(rect);
3085+
3086+
auto embeddedViewParams1 = std::make_unique<flutter::EmbeddedViewParams>(
3087+
screenScaleMatrix, SkSize::Make(10, 10), stack1);
3088+
3089+
flutter::MutatorsStack stack2;
3090+
stack2.PushClipRect(rect);
3091+
auto embeddedViewParams2 = std::make_unique<flutter::EmbeddedViewParams>(
3092+
screenScaleMatrix, SkSize::Make(10, 10), stack2);
3093+
3094+
flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1));
3095+
flutterPlatformViewsController->CompositeEmbeddedView(1);
3096+
UIView* childClippingView = gMockPlatformView.superview.superview;
3097+
3098+
UIView* maskView = childClippingView.maskView;
3099+
XCTAssert([maskView.layer isKindOfClass:[CAShapeLayer class]],
3100+
@"Mask view must use CAShapeLayer as its backing layer.");
3101+
}
3102+
29923103
// Return true if a correct visual effect view is found. It also implies all the validation in this
29933104
// method passes.
29943105
//

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,12 @@ @interface FlutterClippingMaskView ()
236236
// information about screen scale.
237237
@property(nonatomic) CATransform3D reverseScreenScale;
238238

239-
- (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
239+
- (void)addTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
240240

241241
@end
242242

243243
@implementation FlutterClippingMaskView {
244-
std::vector<fml::CFRef<CGPathRef>> paths_;
244+
CGMutablePathRef pathSoFar_;
245245
}
246246

247247
- (instancetype)initWithFrame:(CGRect)frame {
@@ -252,15 +252,31 @@ - (instancetype)initWithFrame:(CGRect)frame screenScale:(CGFloat)screenScale {
252252
if (self = [super initWithFrame:frame]) {
253253
self.backgroundColor = UIColor.clearColor;
254254
_reverseScreenScale = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);
255+
pathSoFar_ = CGPathCreateMutable();
255256
}
256257
return self;
257258
}
258259

260+
+ (Class)layerClass {
261+
return [CAShapeLayer class];
262+
}
263+
264+
- (CAShapeLayer*)shapeLayer {
265+
return (CAShapeLayer*)self.layer;
266+
}
267+
259268
- (void)reset {
260-
paths_.clear();
269+
CGPathRelease(pathSoFar_);
270+
pathSoFar_ = CGPathCreateMutable();
271+
[self shapeLayer].path = nil;
261272
[self setNeedsDisplay];
262273
}
263274

275+
- (void)dealloc {
276+
CGPathRelease(pathSoFar_);
277+
[super dealloc];
278+
}
279+
264280
// In some scenarios, when we add this view as a maskView of the ChildClippingView, iOS added
265281
// this view as a subview of the ChildClippingView.
266282
// This results this view blocking touch events on the ChildClippingView.
@@ -270,28 +286,13 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
270286
return NO;
271287
}
272288

273-
- (void)drawRect:(CGRect)rect {
274-
CGContextRef context = UIGraphicsGetCurrentContext();
275-
CGContextSaveGState(context);
276-
277-
// For mask view, only the alpha channel is used.
278-
CGContextSetAlpha(context, 1);
279-
280-
for (size_t i = 0; i < paths_.size(); i++) {
281-
CGContextAddPath(context, paths_.at(i));
282-
CGContextClip(context);
283-
}
284-
CGContextFillRect(context, rect);
285-
CGContextRestoreGState(context);
286-
}
287-
288289
- (void)clipRect:(const SkRect&)clipSkRect matrix:(const SkMatrix&)matrix {
289290
CGRect clipRect = flutter::GetCGRectFromSkRect(clipSkRect);
290291
CGPathRef path = CGPathCreateWithRect(clipRect, nil);
291292
// The `matrix` is based on the physical pixels, convert it to UIKit points.
292293
CATransform3D matrixInPoints =
293294
CATransform3DConcat(flutter::GetCATransform3DFromSkMatrix(matrix), _reverseScreenScale);
294-
paths_.push_back([self getTransformedPath:path matrix:matrixInPoints]);
295+
[self addTransformedPath:path matrix:matrixInPoints];
295296
}
296297

297298
- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const SkMatrix&)matrix {
@@ -360,7 +361,7 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const SkMatrix&)matrix {
360361
// TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that
361362
// the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge
362363
// clipping on iOS.
363-
paths_.push_back([self getTransformedPath:pathRef matrix:matrixInPoints]);
364+
[self addTransformedPath:pathRef matrix:matrixInPoints];
364365
}
365366

366367
- (void)clipPath:(const SkPath&)path matrix:(const SkMatrix&)matrix {
@@ -425,15 +426,15 @@ - (void)clipPath:(const SkPath&)path matrix:(const SkMatrix&)matrix {
425426
// The `matrix` is based on the physical pixels, convert it to UIKit points.
426427
CATransform3D matrixInPoints =
427428
CATransform3DConcat(flutter::GetCATransform3DFromSkMatrix(matrix), _reverseScreenScale);
428-
paths_.push_back([self getTransformedPath:pathRef matrix:matrixInPoints]);
429+
[self addTransformedPath:pathRef matrix:matrixInPoints];
429430
}
430431

431-
- (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix {
432+
- (void)addTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix {
432433
CGAffineTransform affine =
433434
CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42);
434-
CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine);
435+
CGPathAddPath(pathSoFar_, &affine, path);
436+
[self shapeLayer].path = pathSoFar_;
435437
CGPathRelease(path);
436-
return fml::CFRef<CGPathRef>(transformedPath);
437438
}
438439

439440
@end
8.64 KB
Loading
Loading
Loading
Loading
8.27 KB
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)