Engine 1.20.2 cherrypicks (#20446)
* Update 1.20.2 engine to use Dart 2.9.1
* Use a single mask view to clip iOS platform view (#20050)
* Moved to RMSE for image comparison to account for slight variations in golden image tests (#19658)
Moved to RMSE for image comparison to account for slight variations in golden image production. (also fixed a flakey test)
Co-authored-by: Chris Yang <ychris@google.com>
Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com>
diff --git a/DEPS b/DEPS
index dc297a6..09e6e35 100644
--- a/DEPS
+++ b/DEPS
@@ -34,7 +34,7 @@
# Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS.
# You can use //tools/dart/create_updated_flutter_deps.py to produce
# updated revision list of existing dependencies.
- 'dart_revision': '6eb17654b6501e2617c67854ed113ab550d2b3c7',
+ 'dart_revision': 'e940ff7819053ed8a4c04a4dfcda7df12e969331',
# WARNING: DO NOT EDIT MANUALLY
# The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
index abf854f..e49e0fa 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
@@ -167,7 +167,11 @@
touch_interceptors_[viewId] =
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
- root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]);
+
+ ChildClippingView* clipping_view =
+ [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
+ [clipping_view addSubview:touch_interceptor];
+ root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]);
result(nil);
}
@@ -317,83 +321,60 @@
return clipCount;
}
-UIView* FlutterPlatformViewsController::ReconstructClipViewsChain(int number_of_clips,
- UIView* platform_view,
- UIView* head_clip_view) {
- NSInteger indexInFlutterView = -1;
- if (head_clip_view.superview) {
- // TODO(cyanglaz): potentially cache the index of oldPlatformViewRoot to make this a O(1).
- // https://github.com/flutter/flutter/issues/35023
- indexInFlutterView = [flutter_view_.get().subviews indexOfObject:head_clip_view];
- [head_clip_view removeFromSuperview];
- }
- UIView* head = platform_view;
- int clipIndex = 0;
- // Re-use as much existing clip views as needed.
- while (head != head_clip_view && clipIndex < number_of_clips) {
- head = head.superview;
- clipIndex++;
- }
- // If there were not enough existing clip views, add more.
- while (clipIndex < number_of_clips) {
- ChildClippingView* clippingView =
- [[[ChildClippingView alloc] initWithFrame:flutter_view_.get().bounds] autorelease];
- [clippingView addSubview:head];
- head = clippingView;
- clipIndex++;
- }
- [head removeFromSuperview];
-
- if (indexInFlutterView > -1) {
- // The chain was previously attached; attach it to the same position.
- [flutter_view_.get() insertSubview:head atIndex:indexInFlutterView];
- }
- return head;
-}
-
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
UIView* embedded_view) {
FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity));
- UIView* head = embedded_view;
- ResetAnchor(head.layer);
+ ResetAnchor(embedded_view.layer);
+ ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
- std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
- while (iter != mutators_stack.Top()) {
- switch ((*iter)->GetType()) {
- case transform: {
- CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix());
- head.layer.transform = CATransform3DConcat(head.layer.transform, transform);
- break;
- }
- case clip_rect:
- case clip_rrect:
- case clip_path: {
- ChildClippingView* clipView = (ChildClippingView*)head.superview;
- clipView.layer.transform = CATransform3DIdentity;
- [clipView setClip:(*iter)->GetType()
- rect:(*iter)->GetRect()
- rrect:(*iter)->GetRRect()
- path:(*iter)->GetPath()];
- ResetAnchor(clipView.layer);
- head = clipView;
- break;
- }
- case opacity:
- embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
- break;
- }
- ++iter;
- }
- // Reverse scale based on screen scale.
- //
// The UIKit frame is set based on the logical resolution instead of physical.
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
// resolution. So we need to scale down to match UIKit's logical resolution.
CGFloat screenScale = [UIScreen mainScreen].scale;
- head.layer.transform = CATransform3DConcat(
- head.layer.transform, CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1));
+ CATransform3D finalTransform = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);
+
+ // Mask view needs to be full screen because we might draw platform view pixels outside of the
+ // `ChildClippingView`. Since the mask view's frame will be based on the `clipView`'s coordinate
+ // system, we need to convert the flutter_view's frame to the clipView's coordinate system. The
+ // mask view is not displayed on the screen.
+ CGRect maskViewFrame = [flutter_view_ convertRect:flutter_view_.get().frame toView:clipView];
+ FlutterClippingMaskView* maskView =
+ [[[FlutterClippingMaskView alloc] initWithFrame:maskViewFrame] autorelease];
+ auto iter = mutators_stack.Begin();
+ while (iter != mutators_stack.End()) {
+ switch ((*iter)->GetType()) {
+ case transform: {
+ CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix());
+ finalTransform = CATransform3DConcat(transform, finalTransform);
+ break;
+ }
+ case clip_rect:
+ [maskView clipRect:(*iter)->GetRect() matrix:finalTransform];
+ break;
+ case clip_rrect:
+ [maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform];
+ break;
+ case clip_path:
+ [maskView clipPath:(*iter)->GetPath() matrix:finalTransform];
+ break;
+ case opacity:
+ embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
+ break;
+ }
+ ++iter;
+ }
+ // Reverse the offset of the clipView.
+ // The clipView's frame includes the final translate of the final transform matrix.
+ // So we need to revese this translate so the platform view can layout at the correct offset.
+ //
+ // Note that we don't apply this transform matrix the clippings because clippings happen on the
+ // mask view, whose origin is alwasy (0,0) to the flutter_view.
+ CATransform3D reverseTranslate =
+ CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0);
+ embedded_view.layer.transform = CATransform3DConcat(finalTransform, reverseTranslate);
+ clipView.maskView = maskView;
}
void FlutterPlatformViewsController::CompositeWithParams(int view_id,
@@ -406,17 +387,15 @@
touchInterceptor.alpha = 1;
const MutatorsStack& mutatorStack = params.mutatorsStack();
- int currentClippingCount = CountClips(mutatorStack);
- int previousClippingCount = clip_count_[view_id];
- if (currentClippingCount != previousClippingCount) {
- clip_count_[view_id] = currentClippingCount;
- // If we have a different clipping count in this frame, we need to reconstruct the
- // ClippingChildView chain to prepare for `ApplyMutators`.
- UIView* oldPlatformViewRoot = root_views_[view_id].get();
- UIView* newPlatformViewRoot =
- ReconstructClipViewsChain(currentClippingCount, touchInterceptor, oldPlatformViewRoot);
- root_views_[view_id] = fml::scoped_nsobject<UIView>([newPlatformViewRoot retain]);
- }
+ UIView* clippingView = root_views_[view_id].get();
+ // The frame of the clipping view should be the final bounding rect.
+ // Because the translate matrix in the Mutator Stack also includes the offset,
+ // when we apply the transforms matrix in |ApplyMutators|, we need
+ // to remember to do a reverse translate.
+ const SkRect& rect = params.finalBoundingRect();
+ CGFloat screenScale = [UIScreen mainScreen].scale;
+ clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
+ rect.width() / screenScale, rect.height() / screenScale);
ApplyMutators(mutatorStack, touchInterceptor);
}
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
index e2a0088..0e8397e 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
@@ -14,6 +14,7 @@
FLUTTER_ASSERT_NOT_ARC
@class FlutterPlatformViewsTestMockPlatformView;
static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil;
+const float kFloatCompareEpsilon = 0.001;
@interface FlutterPlatformViewsTestMockPlatformView : UIView
@end
@@ -143,4 +144,385 @@
flutterPlatformViewsController->Reset();
}
+- (void)testChildClippingViewHitTests {
+ ChildClippingView* childClippingView =
+ [[[ChildClippingView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
+ UIView* childView = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)] autorelease];
+ [childClippingView addSubview:childView];
+
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(50, 50) withEvent:nil]);
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(99, 100) withEvent:nil]);
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(100, 99) withEvent:nil]);
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(201, 200) withEvent:nil]);
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(200, 201) withEvent:nil]);
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(99, 200) withEvent:nil]);
+ XCTAssertFalse([childClippingView pointInside:CGPointMake(200, 299) withEvent:nil]);
+
+ XCTAssertTrue([childClippingView pointInside:CGPointMake(150, 150) withEvent:nil]);
+ XCTAssertTrue([childClippingView pointInside:CGPointMake(100, 100) withEvent:nil]);
+ XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 100) withEvent:nil]);
+ XCTAssertTrue([childClippingView pointInside:CGPointMake(100, 199) withEvent:nil]);
+ XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]);
+}
+
+- (void)testCompositePlatformView {
+ 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 platform_view = std::make_unique<flutter::PlatformViewIOS>(
+ /*delegate=*/mock_delegate,
+ /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+ /*task_runners=*/runners);
+
+ auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+ flutterPlatformViewsController->RegisterViewFactory(
+ factory, @"MockFlutterPlatformView",
+ FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+ FlutterResult result = ^(id result) {
+ };
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ XCTAssertNotNil(gMockPlatformView);
+
+ UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
+ flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+ // Create embedded view params
+ flutter::MutatorsStack stack;
+ // Layer tree always pushes a screen scale factor to the stack
+ SkMatrix screenScaleMatrix =
+ SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
+ stack.PushTransform(screenScaleMatrix);
+ // Push a translate matrix
+ SkMatrix translateMatrix = SkMatrix::MakeTrans(100, 100);
+ stack.PushTransform(translateMatrix);
+ SkMatrix finalMatrix;
+ finalMatrix.setConcat(screenScaleMatrix, translateMatrix);
+
+ auto embeddedViewParams =
+ std::make_unique<flutter::EmbeddedViewParams>(finalMatrix, SkSize::Make(300, 300), stack);
+
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
+ flutterPlatformViewsController->CompositeEmbeddedView(2);
+ CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds
+ toView:mockFlutterView];
+ XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300)));
+ flutterPlatformViewsController->Reset();
+}
+
+- (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView {
+ 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 platform_view = std::make_unique<flutter::PlatformViewIOS>(
+ /*delegate=*/mock_delegate,
+ /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+ /*task_runners=*/runners);
+
+ auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+ flutterPlatformViewsController->RegisterViewFactory(
+ factory, @"MockFlutterPlatformView",
+ FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+ FlutterResult result = ^(id result) {
+ };
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ XCTAssertNotNil(gMockPlatformView);
+
+ UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
+ flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+ // Create embedded view params
+ flutter::MutatorsStack stack;
+ // Layer tree always pushes a screen scale factor to the stack
+ SkMatrix screenScaleMatrix =
+ SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
+ stack.PushTransform(screenScaleMatrix);
+ // Push a rotate matrix
+ SkMatrix rotateMatrix;
+ rotateMatrix.setRotate(10);
+ stack.PushTransform(rotateMatrix);
+ SkMatrix finalMatrix;
+ finalMatrix.setConcat(screenScaleMatrix, rotateMatrix);
+
+ auto embeddedViewParams =
+ std::make_unique<flutter::EmbeddedViewParams>(finalMatrix, SkSize::Make(300, 300), stack);
+
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
+ flutterPlatformViewsController->CompositeEmbeddedView(2);
+ CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds
+ toView:mockFlutterView];
+ XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
+ ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
+ // The childclippingview's frame is set based on flow, but the platform view's frame is set based
+ // on quartz. Although they should be the same, but we should tolerate small floating point
+ // errors.
+ XCTAssertLessThan(fabs(platformViewRectInFlutterView.origin.x - childClippingView.frame.origin.x),
+ kFloatCompareEpsilon);
+ XCTAssertLessThan(fabs(platformViewRectInFlutterView.origin.y - childClippingView.frame.origin.y),
+ kFloatCompareEpsilon);
+ XCTAssertLessThan(
+ fabs(platformViewRectInFlutterView.size.width - childClippingView.frame.size.width),
+ kFloatCompareEpsilon);
+ XCTAssertLessThan(
+ fabs(platformViewRectInFlutterView.size.height - childClippingView.frame.size.height),
+ kFloatCompareEpsilon);
+
+ flutterPlatformViewsController->Reset();
+}
+
+- (void)testClipRect {
+ 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 platform_view = std::make_unique<flutter::PlatformViewIOS>(
+ /*delegate=*/mock_delegate,
+ /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+ /*task_runners=*/runners);
+
+ auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+ flutterPlatformViewsController->RegisterViewFactory(
+ factory, @"MockFlutterPlatformView",
+ FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+ FlutterResult result = ^(id result) {
+ };
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ XCTAssertNotNil(gMockPlatformView);
+
+ UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
+ flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+ // Create embedded view params
+ flutter::MutatorsStack stack;
+ // Layer tree always pushes a screen scale factor to the stack
+ SkMatrix screenScaleMatrix =
+ SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
+ stack.PushTransform(screenScaleMatrix);
+ // Push a clip rect
+ SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3);
+ stack.PushClipRect(rect);
+
+ auto embeddedViewParams =
+ std::make_unique<flutter::EmbeddedViewParams>(screenScaleMatrix, SkSize::Make(10, 10), stack);
+
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
+ flutterPlatformViewsController->CompositeEmbeddedView(2);
+ gMockPlatformView.backgroundColor = UIColor.redColor;
+ XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
+ ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
+ [mockFlutterView addSubview:childClippingView];
+
+ [mockFlutterView setNeedsLayout];
+ [mockFlutterView layoutIfNeeded];
+
+ 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);
+ }
+ }
+ }
+ flutterPlatformViewsController->Reset();
+}
+
+- (void)testClipRRect {
+ 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 platform_view = std::make_unique<flutter::PlatformViewIOS>(
+ /*delegate=*/mock_delegate,
+ /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+ /*task_runners=*/runners);
+
+ auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+ flutterPlatformViewsController->RegisterViewFactory(
+ factory, @"MockFlutterPlatformView",
+ FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+ FlutterResult result = ^(id result) {
+ };
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ XCTAssertNotNil(gMockPlatformView);
+
+ UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
+ flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+ // Create embedded view params
+ flutter::MutatorsStack stack;
+ // Layer tree always pushes a screen scale factor to the stack
+ SkMatrix screenScaleMatrix =
+ SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
+ stack.PushTransform(screenScaleMatrix);
+ // Push a clip rrect
+ SkRRect rrect = SkRRect::MakeRectXY(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1);
+ stack.PushClipRRect(rrect);
+
+ auto embeddedViewParams =
+ std::make_unique<flutter::EmbeddedViewParams>(screenScaleMatrix, SkSize::Make(10, 10), stack);
+
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
+ flutterPlatformViewsController->CompositeEmbeddedView(2);
+ gMockPlatformView.backgroundColor = UIColor.redColor;
+ XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
+ ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
+ [mockFlutterView addSubview:childClippingView];
+
+ [mockFlutterView setNeedsLayout];
+ [mockFlutterView layoutIfNeeded];
+
+ 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)) {
+ XCTAssertEqual(alpha, 255);
+ } else {
+ XCTAssertLessThan(alpha, 255);
+ }
+ }
+ }
+ flutterPlatformViewsController->Reset();
+}
+
+- (void)testClipPath {
+ 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 platform_view = std::make_unique<flutter::PlatformViewIOS>(
+ /*delegate=*/mock_delegate,
+ /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+ /*task_runners=*/runners);
+
+ auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();
+
+ FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+ [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+ flutterPlatformViewsController->RegisterViewFactory(
+ factory, @"MockFlutterPlatformView",
+ FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+ FlutterResult result = ^(id result) {
+ };
+ flutterPlatformViewsController->OnMethodCall(
+ [FlutterMethodCall
+ methodCallWithMethodName:@"create"
+ arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
+ result);
+
+ XCTAssertNotNil(gMockPlatformView);
+
+ UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease];
+ flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+ // Create embedded view params
+ flutter::MutatorsStack stack;
+ // Layer tree always pushes a screen scale factor to the stack
+ SkMatrix screenScaleMatrix =
+ SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
+ stack.PushTransform(screenScaleMatrix);
+ // Push a clip path
+ SkPath path;
+ path.addRoundRect(SkRect::MakeXYWH(2, 2, 6, 6), 1, 1);
+ stack.PushClipPath(path);
+
+ auto embeddedViewParams =
+ std::make_unique<flutter::EmbeddedViewParams>(screenScaleMatrix, SkSize::Make(10, 10), stack);
+
+ flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
+ flutterPlatformViewsController->CompositeEmbeddedView(2);
+ gMockPlatformView.backgroundColor = UIColor.redColor;
+ XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]);
+ ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview;
+ [mockFlutterView addSubview:childClippingView];
+
+ [mockFlutterView setNeedsLayout];
+ [mockFlutterView layoutIfNeeded];
+
+ 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)) {
+ XCTAssertEqual(alpha, 255);
+ } else {
+ XCTAssertLessThan(alpha, 255);
+ }
+ }
+ }
+ flutterPlatformViewsController->Reset();
+}
+
+- (int)alphaOfPoint:(CGPoint)point onView:(UIView*)view {
+ unsigned char pixel[4] = {0};
+
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+
+ // Draw the pixel on `point` in the context.
+ CGContextRef context = CGBitmapContextCreate(
+ pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast);
+ CGContextTranslateCTM(context, -point.x, -point.y);
+ [view.layer renderInContext:context];
+
+ CGContextRelease(context);
+ CGColorSpaceRelease(colorSpace);
+ // Get the alpha from the pixel that we just rendered.
+ return pixel[3];
+}
+
@end
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
index 7a4724f..796d1e5 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
@@ -16,6 +16,33 @@
#include "flutter/shell/platform/darwin/ios/ios_context.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
+// A UIView that acts as a clipping mask for the |ChildClippingView|.
+//
+// On the [UIView drawRect:] method, this view performs a series of clipping operations and sets the
+// alpha channel to the final resulting area to be 1; it also sets the "clipped out" area's alpha
+// channel to be 0.
+//
+// When a UIView sets a |FlutterClippingMaskView| as its `maskView`, the alpha channel of the UIView
+// is replaced with the alpha channel of the |FlutterClippingMaskView|.
+@interface FlutterClippingMaskView : UIView
+
+// Adds a clip rect operation to the queue.
+//
+// The `clipSkRect` is transformed with the `matrix` before adding to the queue.
+- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix;
+
+// Adds a clip rrect operation to the queue.
+//
+// The `clipSkRRect` is transformed with the `matrix` before adding to the queue.
+- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix;
+
+// Adds a clip path operation to the queue.
+//
+// The `path` is transformed with the `matrix` before adding to the queue.
+- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix;
+
+@end
+
// A UIView that is used as the parent for embedded UIViews.
//
// This view has 2 roles:
@@ -37,14 +64,6 @@
// The parent view handles clipping to its subviews.
@interface ChildClippingView : UIView
-// Performs the clipping based on the type.
-//
-// The `type` must be one of the 3: clip_rect, clip_rrect, clip_path.
-- (void)setClip:(flutter::MutatorType)type
- rect:(const SkRect&)rect
- rrect:(const SkRRect&)rrect
- path:(const SkPath&)path;
-
@end
namespace flutter {
@@ -253,20 +272,6 @@
// Traverse the `mutators_stack` and return the number of clip operations.
int CountClips(const MutatorsStack& mutators_stack);
- // Make sure that platform_view has exactly clip_count ChildClippingView ancestors.
- //
- // Existing ChildClippingViews are re-used. If there are currently more ChildClippingView
- // ancestors than needed, the extra views are detached. If there are less ChildClippingView
- // ancestors than needed, new ChildClippingViews will be added.
- //
- // If head_clip_view was attached as a subview to FlutterView, the head of the newly constructed
- // ChildClippingViews chain is attached to FlutterView in the same position.
- //
- // Returns the new head of the clip views chain.
- UIView* ReconstructClipViewsChain(int number_of_clips,
- UIView* platform_view,
- UIView* head_clip_view);
-
// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
// `ReconstructClipViewsChain`
//
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
index 551535a..5e9ed80 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm
@@ -53,32 +53,72 @@
@implementation ChildClippingView
-+ (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect {
- return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft,
- clipSkRect.fBottom - clipSkRect.fTop);
+// The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to
+// be hit tested and consumed by this view if they are inside the embedded platform view which could
+// be smaller the embedded platform view is rotated.
+- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
+ for (UIView* view in self.subviews) {
+ if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
+ return YES;
+ }
+ }
+ return NO;
}
-- (void)clipRect:(const SkRect&)clipSkRect {
- CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect];
- fml::CFRef<CGPathRef> pathRef(CGPathCreateWithRect(clipRect, nil));
- CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
- clip.path = pathRef;
- self.layer.mask = clip;
+@end
+
+@interface FlutterClippingMaskView ()
+
+- (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
+- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect;
+
+@end
+
+@implementation FlutterClippingMaskView {
+ std::vector<fml::CFRef<CGPathRef>> paths_;
}
-- (void)clipRRect:(const SkRRect&)clipSkRRect {
+- (instancetype)initWithFrame:(CGRect)frame {
+ if ([super initWithFrame:frame]) {
+ self.backgroundColor = UIColor.clearColor;
+ }
+ return self;
+}
+
+- (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 CATransform3D&)matrix {
+ CGRect clipRect = [self getCGRectFromSkRect:clipSkRect];
+ CGPathRef path = CGPathCreateWithRect(clipRect, nil);
+ paths_.push_back([self getTransformedPath:path matrix:matrix]);
+}
+
+- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix {
CGPathRef pathRef = nullptr;
switch (clipSkRRect.getType()) {
case SkRRect::kEmpty_Type: {
break;
}
case SkRRect::kRect_Type: {
- [self clipRect:clipSkRRect.rect()];
+ [self clipRect:clipSkRRect.rect() matrix:matrix];
return;
}
case SkRRect::kOval_Type:
case SkRRect::kSimple_Type: {
- CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRRect.rect()];
+ CGRect clipRect = [self getCGRectFromSkRect:clipSkRRect.rect()];
pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(),
clipSkRRect.getSimpleRadii().y(), nil);
break;
@@ -129,23 +169,17 @@
// 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.
- CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
- clip.path = pathRef;
- self.layer.mask = clip;
- CGPathRelease(pathRef);
+ paths_.push_back([self getTransformedPath:pathRef matrix:matrix]);
}
-- (void)clipPath:(const SkPath&)path {
+- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix {
if (!path.isValid()) {
return;
}
- fml::CFRef<CGMutablePathRef> pathRef(CGPathCreateMutable());
if (path.isEmpty()) {
- CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
- clip.path = pathRef;
- self.layer.mask = clip;
return;
}
+ CGMutablePathRef pathRef = CGPathCreateMutable();
// Loop through all verbs and translate them into CGPath
SkPath::Iter iter(path, true);
@@ -197,42 +231,20 @@
}
verb = iter.next(pts);
}
-
- CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
- clip.path = pathRef;
- self.layer.mask = clip;
+ paths_.push_back([self getTransformedPath:pathRef matrix:matrix]);
}
-- (void)setClip:(flutter::MutatorType)type
- rect:(const SkRect&)rect
- rrect:(const SkRRect&)rrect
- path:(const SkPath&)path {
- FML_CHECK(type == flutter::clip_rect || type == flutter::clip_rrect ||
- type == flutter::clip_path);
- switch (type) {
- case flutter::clip_rect:
- [self clipRect:rect];
- break;
- case flutter::clip_rrect:
- [self clipRRect:rrect];
- break;
- case flutter::clip_path:
- [self clipPath:path];
- break;
- default:
- break;
- }
+- (fml::CFRef<CGPathRef>)getTransformedPath:(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);
+ CGPathRelease(path);
+ return fml::CFRef<CGPathRef>(transformedPath);
}
-// The ChildClippingView is as big as the FlutterView, we only want touches to be hit tested and
-// consumed by this view if they are inside the smaller child view.
-- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
- for (UIView* view in self.subviews) {
- if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
- return YES;
- }
- }
- return NO;
+- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect {
+ return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft,
+ clipSkRect.fBottom - clipSkRect.fTop);
}
@end
diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m
index e1b27c9..9961d1a 100644
--- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m
+++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m
@@ -6,6 +6,8 @@
#import <XCTest/XCTest.h>
#include <sys/sysctl.h>
+static const double kRmseThreshold = 0.5;
+
@interface GoldenImage ()
@end
@@ -67,8 +69,24 @@
CGContextDrawImage(contextB, CGRectMake(0, 0, widthA, heightA), imageRefB);
CGContextRelease(contextB);
- BOOL isSame = memcmp(rawA.mutableBytes, rawB.mutableBytes, size) == 0;
- return isSame;
+ const char* apos = rawA.mutableBytes;
+ const char* bpos = rawB.mutableBytes;
+ double sum = 0.0;
+ for (size_t i = 0; i < size; ++i, ++apos, ++bpos) {
+ // Skip transparent pixels.
+ if (*apos == 0 && *bpos == 0 && i % 4 == 0) {
+ i += 3;
+ apos += 3;
+ bpos += 3;
+ } else {
+ double aval = *apos;
+ double bval = *bpos;
+ double diff = aval - bval;
+ sum += diff * diff;
+ }
+ }
+ double rmse = sqrt(sum / size);
+ return rmse <= kRmseThreshold;
}
NS_INLINE NSString* _platformName() {
diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m
index 3752203..2d3db20 100644
--- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m
+++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m
@@ -243,12 +243,8 @@
XCUIElement* overlay = app.otherElements[@"platform_view[0].overlay[0]"];
XCTAssertTrue(overlay.exists);
- XCTAssertEqual(overlay.frame.origin.x, 75);
- XCTAssertEqual(overlay.frame.origin.y, 85);
- XCTAssertEqual(overlay.frame.size.width, 150);
- XCTAssertEqual(overlay.frame.size.height, 190);
-
XCTAssertFalse(app.otherElements[@"platform_view[0].overlay[1]"].exists);
+ XCTAssertTrue(CGRectContainsRect(platform_view.frame, overlay.frame));
}
@end
diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png
index 9ec19ab..30072dc 100644
--- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png
+++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png
Binary files differ
diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png
index b193419..69ba03a 100644
--- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png
+++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone 8_simulator.png
Binary files differ