From 7fad84e4db9961e2a5b56051300075aa4c94b503 Mon Sep 17 00:00:00 2001 From: pcsosinski Date: Wed, 12 Aug 2020 08:58:36 -0700 Subject: [PATCH 1/3] Update 1.20.2 engine to use Dart 2.9.1 --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index dc297a638b494..09e6e358b99d2 100644 --- a/DEPS +++ b/DEPS @@ -34,7 +34,7 @@ vars = { # 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 From ea61df9f684507a53a3ec6106766354f058621d2 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 3 Aug 2020 10:07:19 -0700 Subject: [PATCH 2/3] Use a single mask view to clip iOS platform view (#20050) --- .../framework/Source/FlutterPlatformViews.mm | 119 +++--- .../Source/FlutterPlatformViewsTest.mm | 382 ++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.h | 49 ++- .../Source/FlutterPlatformViews_Internal.mm | 118 +++--- ...tform_view_clippath_iPhone 8_simulator.png | Bin 20295 -> 20863 bytes ...form_view_cliprrect_iPhone 8_simulator.png | Bin 19558 -> 19543 bytes 6 files changed, 523 insertions(+), 145 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index abf854f15731b..e49e0fa78cdb5 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([touch_interceptor retain]); - root_views_[viewId] = fml::scoped_nsobject([touch_interceptor retain]); + + ChildClippingView* clipping_view = + [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease]; + [clipping_view addSubview:touch_interceptor]; + root_views_[viewId] = fml::scoped_nsobject([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>::const_reverse_iterator iter = mutators_stack.Bottom(); - while (iter != mutators_stack.Top()) { + // 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; + 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()); - head.layer.transform = CATransform3DConcat(head.layer.transform, transform); + finalTransform = CATransform3DConcat(transform, finalTransform); break; } case clip_rect: + [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; + break; 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; + [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 scale based on screen scale. + // 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. // - // 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)); + // 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([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 e2a0088dfc96b..0e8397e1146b4 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 @@ - (void)testCanCreatePlatformViewWithoutFlutterView { 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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + 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(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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + 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(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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + 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(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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + 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(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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + + auto flutterPlatformViewsController = std::make_unique(); + + 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(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 7a4724f5b0c6b..796d1e5d14bcc 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 @@ class FlutterPlatformViewsController { // 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 551535a2c7faf..5e9ed80279975 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 @@ void ResetAnchor(CALayer* layer) { @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; +} + +@end + +@interface FlutterClippingMaskView () + +- (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix; +- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect; + +@end + +@implementation FlutterClippingMaskView { + std::vector> paths_; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if ([super initWithFrame:frame]) { + self.backgroundColor = UIColor.clearColor; + } + return self; } -- (void)clipRect:(const SkRect&)clipSkRect { - CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect]; - fml::CFRef pathRef(CGPathCreateWithRect(clipRect, nil)); - CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease]; - clip.path = pathRef; - self.layer.mask = clip; +- (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 { +- (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 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect { // 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 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 @@ - (void)clipPath:(const SkPath&)path { } 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)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(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/golden_platform_view_clippath_iPhone 8_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone 8_simulator.png index 9ec19ab474f03b6cc7786cb038ee3b3e6d0c51a4..30072dc62164626aad700f7153f9a56b64192038 100644 GIT binary patch literal 20863 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 literal 20295 zcmeHuX*iVa8@HyJnz3Yzr3E$ieMzzvCQH_kA+oDbk8B}3O<5*oDSL~pkUjg(q>u%s=030UI)CT+JD2M|+}G1lqoY1XO+`gTr=hN* zPelcrrJ_PeAz|PX)smMN!DXMTzM2wMUK`sa_>ZTJv4-u13si#O9!Z7RhoRaJJp%qx z?K?(=pxjeYY3^hF_g;S=|DR{Tnjtr+;D4Sm0axfxB=`Z+f3C1(*xx5$v&sAa-Xo-- zN15f^xWEPFtbW;*ii(8?`q`(U&pi)5WV@lFYXGj$#-Yno@DKm)6}p!srTj#Pg6l&K zm2(E3`=@BVmW3Qoo zEAE=V()MiS$ENStBiA{d&TI4$Uu9`u;g8Qjx^L@!$Qxhsl75%>84gx2Zkt9p zRMxIl)orxYi*C=;2XrpKI37oy58lCFSM-=&@xF=oTnrW3#;5$UiQAH?R`gyXGY?cu zrq>A6T!x{2ge|-jD(&p3OzZK`!Cx68%Q=PKgb{}sqw2|M$ zql9LA7VneSbIERwo4HO~Pv=4E+IARkA+J5RTrU5ixIMDd-Gj|3_n70e{kVRPqm%YbIrEf=)%WS0xw@4K>=zfOA>tKgx*wpiLK}#s8`F)Z8tF7a!t7z7<4>CBITKz z!{_m5FWzUiD$~o$dd}A|mHK|YyyH4I(Bw_OI$M<_R7_9vIen|xbfYUnVx`aZZ%(5}4Y;&(4*>&L_jytIj{Ezty%+*$Wvj~__PCB!-R41mQZ@4VB z(*JF1Rc{KP5FV~<_snHf?8gPCb<3b_3%5`07E%0D?E@JS){IU*z3tSj^fc0a9lab$ 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 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 b193419929e7b4666e8b753d67d769ea26424595..69ba03a131136f38e4d7e23a8b3349aafd15dea7 100644 GIT binary patch literal 19543 zcmeHvXH-*NyKNGJG?f4%9U@3onsgC}RHaB4r7O~#h!hPi`XU{a-my>w1f(}1s309g zdWRsrNeP607QXkp_s9Kz#u;biM@BZu-fOS*tY^+OpSd;((Yd2`j*^)Y0)d>neM?yn z0)eeTAf%GyFz|^=d8RISgSzXfDMAXnSQo(`%GUR8+h}P)1i&#l1R7)yfuEiNUd-SH zfsnj~LP)?n^z^g0u>X7uTYF3LpJP(V(-S}Ud~$?9P>|coHx0a@tK-jJk9nj?{Yf9Y zMA3Tr^5shumoH7n{=*fZsBC)SS-^{N#k`>r<95Na3e)n>4vvoHrdghYSwjLG8v%L= zPon0j8J-rHg@=&^2f!4S|3F|ol%f=fmYcLZ3`z51+e1m)+nOA`a=lwe^)-6$4t5WY zBfFP-Rj^yKHB_Y3FgQ5^i3)%~i3h|W5HsO_0}2{iD4vFoWyg$c4i#Hz%dVF7YAg6I z{>s{#>pbW_@fjqcPQ91+hl}IcQ~c7rhH#j+-&RUe2BDE7j_0^nVH}MM8C4fPP{?&V z^unTXgY}*^_8ZzhxUX?nEUwnEF0{_e?+yfUnCqm<`W>=VP9Cqe#!$ze!5(hLQu`B5 zV$J;yXB6bOIs~&4=rhKiFIVqQ`un)u?W6BEWWAeA$kjGG>=0|dU{Sy7%;H)#{pQ!F zQ0?PQ;i91m=O=Ch948xMgJ!$!q2EL4Tx*^Cqx_yKZtN}f3;X=$QaBoF78);GT)@Td zVo&B1%yv?6TY3H`Q)C*)vld-K=MMGE-M}z!cV{XqkW*b9XefHWNvo(h; zl4XC!)UL~KWF=%@oEN%ZaFA*OxXHql-oJTEUwi!dOoYNdp;`DowqDF! z$l`cO;dq8RENu7aU}v;@|9CgyN9<5ck?*R1{V~3G@Y8*B)<4}RhcD^w4E17{&*p=$Th9Vrfz3a8;Jw;2uLx~B_2-_=t+zY_q#(9keS zahhn^e{i4cs6O0mH!3oh^Ke+t>7f$V^j~<#ZRp4-O6@upSi{?I`k-$ceRjIFMvpKP1&0dyYLqSh!|kPcQb_O7Y(E0GocB z=q2~*K$aB4zP))@0hJ`*gCA#(gA@)IrmmbEtw$a2`YVhTQTt47gwF74dUMfs&}nx> zRCmisZS@=Rx(>g=oEH})i~!a8qxZ5NI$mh&DQFcKUmS%mcwuNYOA&IcW(Xz z7NGC^A~w=e@k13ncTx%UyLIbZ87GI<31)ZOMBP44&7}z6@cw+=>AtD=@!o`X_9cNP zvVQIaC%%G5?eX@NV^5ATByuOoMFhh5y3Lopcm1+rk)n+^BSX2=)*H#8rM6h}v6DJ< zv+trtzIwT`ONykk#o{a?gVk%yG5@Z!P{D5uS>(y&iP_!f!zZndMOufy61oIuyIKtA zQ{BBcN;aLcYz4DAO9oVyg+oHc>zO-Og=0p`_ zVeUOmwK4x^S@FT@$X>=?$C8nST%NewAz_0z_3^jb$)hzEm#XP27JF>~PSwkWwJsaD zrYvYWpkPaHc%DD*e79eCO+iNnc402XHF8GO%^|mHYolkgg^TKV&UK>PV_8=}n>A5h z?tB;ZPNGD9{`%Kw3CDQn;a_M|+s{o7iMzJ`J7HE)f95cL%FW^1*ZvGey==`qSpYExdGo=4M$d;Ug+KDpwH6KFJ@QaHzg zf$s0b-aF{(suthfG#yIKtXl@gCQZB?hg02c1FX#+xW-=p!{*QkmYiLSM+2W`7=#Y< zlf?Y>_>`lMpS}~po-pbIaq^QA%A3fnFLw}Uv{t`dI8Mb-FX=wF?B>EJ&&Ji9H zwBMJrb`s=YwNQzb^Z6p;DKAtNqVT3qTl#@5>rZB}Z1iAZh^(h(xKHoAA18l04tC13@WeYUYlipWn$~%y{PN>)2Z;BWU8!I#s=xjc)IviH+c|5BI-k@ocsRP$&n7N8fa9 zyg(#tKi$D3Q{Q%}26u*Z27Xa{Qo(&Lz&1>si%Y}Iqravl%~x9^ZZ2Mqzm`cbUT!9Q zLjRUESD5e&`Pd86B7Bvn+$x z8-vs)7Y#EeAAi@LxzVHtkUJx}n>3boyG zUcK~2H%xXgMl<_rgP*IKW!LDhKYlA!-<^TYpVs#-zraprT>mGqcE%yIiob51R5Z(a z#;GUIFlAyZeX=i|7N)y!J$`r4Yj&%yH(i9|)P z)IBH78*kNF-`C?fN$U2o87{GQ6V&i&Z{JysnK>UzxEkWLqmE}+*8e| zX12bbN$D<{!QPH^GrupHxEIdSqKZC!v{GYD{kS5my!^x4sm00izLPy_>k*|!x`R^( zhszcx`(YM`xC-ZU=46tDwoYyoQX8t99SI_*a1w9gl4}K3qChP0en#)EnYh;BZ50mg zV`X>%U%#cnK(Q{FZ{o7*J5(6V${sd__T?LJUeE)`n~kOgjSc*+sIj%Ps0TEpz}VQ>00aE^bib6{|IjU=%Ud6T6j6qHiS$GHn)@); zd>9AVy3f3(jQmruF$at&M01 z<{g!(&qmXrIePWypw&4z`EPz?+wWm7LpnQQjALOn&-w1MLHS`87+f_ZfKN=F*;^`P+9KXS1+H3&VvVc*9j&42b#5mef_ZQOVf~6` zG@BgpQ43jbV}ol9DUXl{g}^ym6KzHy1;Q>3WotX1XP9qX>F3CULY{-_@^7MTA|aSEFqvihxy-OuimRqok0-u7YmLi7f@XP;ZQ>EXLdg+!ZyIFg zQ+wY_oW_lUJop-gxGn{pmf>FY6Kfp3KokUX34Ey3P|6218Rz58R^BKM1;2Ze)?9G% zaCxY%%{IP+ovuX-`J`iWw%tA7xDHO<2SV0IZp{dVaq%O_Y*e3YRcHmESg62MjEKqR?9AGxHz0O-h(#tWH^q|4eAfv~!Pwi`_>$-qGca=7t8XJAw%u*f*9vT?Y~s{QXMrTFb%7GcP0CGW)d3 z5x4GJu8mdmC*(+i)B&+#jw@va1G*xD-dUL*e_KF<6uA!i(bL0=z8%oektBBM6wBBE zzTU~k3o!VIF4Ep982B|*LmL1N4#umkQwjHrDb=o)uvKmTsB|}Yx zSM%h?*h?S?xOkCm;i+|kbc~XiTY~!Y9-1i@p~6bzPdDOkG;nOx7p6d7|!0O>)sPLlkZxYC_rJ{pbSu+rodc=b2dH| zTBH!=!ZV5$?jk!2JI6<*q?g6@d_SAP_JH8r+cwV1rMe)d%o=VV)0{G2QBzqUyyEkf zKSy#gWE}ZA6Hxgh6Z*I`R5z(;bE^2KoEQhOH5o;^g9=}zZfkLNpo;7;K)%5Y@-K^| z!rWBtyX>1xvV+`9J)Ewrc_h0VH}`DSS{#>eX?avJsRy8(VfCw(lDr?t%d$|-u`+B_ zU9QWl`cG2&m};42O_Qf6fUrvD1hP4>N1Z%80Hv0yn_T<;oVLI@I~u8=ZnK`!(Z4rM z51`ZhtKGlzAdCoI3OVJI7kg`rvxP%lS=T1M43sw%h*j9Y?o&t$#2D7nr~HHR@oit| z6m2?xSx1r9m0JaIO9{xl4^eq7s>me7Yt2i;U+@>y-#P<`>2F$W!3+L zO|-$)+TJ*|(q4n`AW{O!&jKAi)GhBUKK|59_|66Im5(*~UnXD?yr{QvU}~>;6r`ZU z;HpPQ>gC7_)Uy58elY@#$qLFq*2QUhj3nK{o?>-K?}$^5ajEPZ8y~c}qg_E9Y6M(6 za%4EZaB*QT`DAA=Yx%+FaM$jw_A}q7oao@cKqd2QDaS#N)_co{QD5L=wlOY8)C^`C z6MGX*z76m>K?xr*cssva7_G!ijGJH_`moh3jaA)(K7%Tk&&yOVorapLpbN0 zlVl8aOoP(`bx-U3sYJCL#hTP|2phWJ?tX()B5$(HGaYP(1YhH_b!~^AK`bOzi|TEm)f1z+`8m1?*uOk*gmq5e&4&Ha(o`d7K z#y;=`e3QjMFw8()GrvB2NyG`}_K;hMfcMsa8}uz=c;YOa+<*>Yokx+RZOKS_J=OTS zp2=s~U*E65-~u4Q>2)+YMXtd%1h1WMjqOpZX#dmh(T4bP8D*&h3LhPIF$yp(zZp43 zt*4+kS(`RZ@YU2UsP5b?>LT*+)PNX%`qi|j`7IAQ{~xSq=!2I{#Po+rI*YQo~* zGif7vL0ypJl3yM{${Nj-Df9TuSJ5yCRQCt^lvgy<2H6jK;?d9?!O@2~bL<&Y5Raf0*FSS7E~-;=@={NZHGV%r4KNu!;#XW#@5rSvAYcAS>VWRXW4! zU@ z#NKg3p86R?iyru@WHr>~3WlP0#KCLC%NAAc>bk;&c>^kTRlUgyifwY~%lb115Q7Mv zS2Qh};QUY1ZO#gq^BF(OB}fgN@2JwW+yS2!&QiL_LI)2AhG=;c9sao%jXfL)8=eTt~G`xE$alXTM=xbdy)yE77MjdpNI}Vz?I#%r-Aj3n) z_@bfSOzECn8rIb)G1xmoo(cG> zucwv4!l(H3?2o#Z>pRS3bMj7DF}G?gX`lAn_n_-2X29|(YA|7k5eQ&LW+ zT!u*nDSPTp#k-DG{h!NGu~5Sg0S`cC1Mmvuo)vL!j%0Ir$lUPb_a$;^S3NHFkIe7R za=lIhty+8^EV8hegsIYf6hIi2?*E9*Q@{DUWb!}Lom;fpEw^jxOU0=5_6%lCXm`)( zfuaFvixZ3qgbpupl4P5n>k9oq(Z~67mM7amGx!urNE6Vd`hjNMBoeaoeU;%o@I1;Y z4i;TQ=&2Qh0xk>0CZP3Tciqd<2Eq~(ghWV*R-4)&unle*gGhNBnidlf!P*n;8ym{w z?e_vuh8VN@ayv^dX_C=Ir2x?HU9{!uNV$iw6%^JFukkge)`Z(yvbd8Y_`rl>DZr*3 zu6A?jZYj8u#BAJ;vo>0fI;>P9?vW-J>Mf|d#W(K|TDARtwLFZ!BvKVXB1Z4)S+C<( z5&{T@53pLq7dm#IKVR3@)>Pxm&SGxDIok*qtFC20hPR#!iwlGF`3Z2cn6JCMRbWyn zkMGkjkCco=MZh+SSIL!=3wse5ApzmZg~sXX|D-8tD|1Pr95v>e~|~= zxX^pG4PUcaxH?|1k&UbUrVJPr3?j~8De0n7$0rMHeL8nIw@wUdcfexa`w?g4EtToh zrTOEjJ(}bYfY~U=R!;gIyi*D~ljwR4qe_ZI=|knM5`gjm$ylQ+$b}0{n$Ph1t|DmgqmLIc zcw(~|Hmqy2#vnVmJ0Ow_!I*>dWLyJZl2K1QX%}=ud%EAX6@>3oI+>^VBrozm-WsV_ z0d5gkO#q!TzssW@1rDxr<@jCSR2ujaeeh^>Z#`U^(r2-iwaBe-H_h?}6cz_4NL?V} z&cUL#|9+>dUujR4pqiy6MtYByOfdQ|FRsd(U)?=-y`fVo2)MeS;jg|+=`0MLV_uWK z9>O+Im?3GjrdSgzoo&KjIq(s1;K#9sc3*tHMZ|Y8l5VLW)CX8d7L!bNkJO7T+F zWZQPXUDp<&XQc{Gm=5A_>00^}h_b*)^=iV5pu1y5+XrftkhqzC z`z-LreL>U=) zH&H0orJ?fb`9B?*Oo5^^BgJbk(KBA6wUu&G+v`m<)+5E(osfoGfB?S_VgTIMjt|x| zBpe661yTjM=2ygqpjgvGQ2Gl(nrlu)p_Q%+etSlNN1c0fjJnx%$&Rk%WkJB4{Ny`S z>$T1mbz_q-6Q%YxNy>HNv&-m@Xog@a``QWyPuCDDF*drK2=D6gTFKo*)|-b_UP?xU zyJP;k*_>e^@M{c@Z99YOpz*>Meogd7pNz{LR_60E6hHAd5+wbr8sv(8alCJT> zI{&a{F78z%R>|_8|6aCI`Cd6mejFeBFHX!RDBZ$(_vSPF*Q)2)!DAb%CE7b&Oc>nF zWa&F%E_z#zq503foLzbzB$m|CTeqQrFhT+>RG?WJ-I7lqBZ$?%6C$QV<2bg_=gG$0 zFMDCvNqZ?==(!nX1ZKN0aU>8FXbOPWcz=$NLV{wQRZ;Ko0iHYf(&Mb3;~9 zW@UCgU=g0NMc8yC(%iJ&-(K7ZL~jJg(orXjlzC8%KAXpF_oi6f>ZRG^Nx$IwJ96GP zYOIka0mm)u)@656xaQ67kMqooPg7)O)r@5tYyMX%5TC94Yis|jy8abKfS8E~#2^q; zf`|el3Wz8mqJW43A_|BoAfkYX0wM~CC?KMMhyo%Ch$tYUfQSMj3WzB9UyXvAO_F&C zgsS}Xw*ZL$fKJ>O`1^PXanFdjLqz;R4k8MOC?KMMhyo%Ch$#3UhXSN9=7buKzxjOn UX?g4Ek9yr!xuaaDX!+v50K5E}D*ylh literal 19558 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 bW=EVquwRNbf&U@~0x8O>-7mOj_VWJ#(2mT- From 2570e69eef236e1eb1a49d6d07a6fa957229dc48 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Mon, 13 Jul 2020 16:14:26 -0700 Subject: [PATCH 3/3] 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) --- .../Scenarios/ScenariosUITests/GoldenImage.m | 22 +++++++++++++++++-- .../UnobstructedPlatformViewTests.m | 6 +---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m index e1b27c9bb845a..9961d1a13faf8 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m @@ -6,6 +6,8 @@ #import #include +static const double kRmseThreshold = 0.5; + @interface GoldenImage () @end @@ -67,8 +69,24 @@ - (BOOL)compareGoldenToImage:(UIImage*)image { 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 375220361a8c6..2d3db20b4e7a9 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/UnobstructedPlatformViewTests.m @@ -243,12 +243,8 @@ - (void)testPlatformViewsMaxOverlays { 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