From 021480d12b377eedd96227902f4f3ab1a13b9876 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Fri, 10 Jun 2022 12:31:51 -0700 Subject: [PATCH 01/51] Resolving merge conflicts. --- DEPS | 4 + ci/licenses_golden/licenses_skia | 4 + .../framework/Source/FlutterPlatformViews.mm | 3 + .../Source/FlutterPlatformViewsTest.mm | 189 ++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.h | 3 + .../Source/FlutterPlatformViews_Internal.mm | 75 +++++-- .../IosUnitTests.xcodeproj/project.pbxproj | 4 +- 7 files changed, 263 insertions(+), 19 deletions(-) diff --git a/DEPS b/DEPS index 5182d8ee36ff3..bfdceb24f3fbd 100644 --- a/DEPS +++ b/DEPS @@ -17,7 +17,11 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', +<<<<<<< HEAD 'skia_revision': '2910d7bf6c04d951d149dc15af165bb1afe78804', +======= + 'skia_revision': 'c83c3459715c4bfae757f1bad5805f6f6423052a', +>>>>>>> d80dd7bfa3 (# This is a combination of 23 commits.) # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 49ddcf8f360e2..c7cdebc2fd3de 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,8 @@ +<<<<<<< HEAD Signature: 86c5103d0a1040cb4fc664d07992329e +======= +Signature: 78b6011f8e217bf28cc67c3f6c492758 +>>>>>>> d80dd7bfa3 (# This is a combination of 23 commits.) UNUSED LICENSES: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 16ae589c32185..6d1c9f66ca746 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import #import #include @@ -384,6 +385,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { if (flutter_view_ == nullptr) { return; } + FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity)); ResetAnchor(embedded_view.layer); ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview; @@ -408,6 +410,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); + [clipView applyBackdropFilterWithRadius:@(5)]; break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index b91880b3a4d09..6ca9dc8398fbb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -3,6 +3,9 @@ // found in the LICENSE file. #import +#import +#import +#import #import #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" @@ -242,6 +245,192 @@ - (void)testChildClippingViewHitTests { XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); } +- (void)testChildClippingViewApplyBackdropFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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); // TODO EMILY: should viewType be backdropFilter? + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + +- (void)testChildClippingViewApplyDuplicateBackdropFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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); // TODO EMILY: should viewType be backdropFilter? + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter + stack.PushTransform(screenScaleMatrix); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + +- (void)testChildClippingViewApplyDuplicateBackdropFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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); // TODO EMILY: should viewType be backdropFilter? + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter + stack.PushTransform(screenScaleMatrix); + + 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]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + - (void)testCompositePlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index fde21d23e2e20..aea92f84ab5af 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -48,6 +48,9 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView +// Adds a blur filter to its layers. +- (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? + @end namespace flutter { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index c662a078f2a16..a53b831084d6e 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -12,20 +12,20 @@ namespace flutter { FlutterPlatformViewLayer::FlutterPlatformViewLayer( - fml::scoped_nsobject overlay_view, - fml::scoped_nsobject overlay_view_wrapper, - std::unique_ptr ios_surface, - std::unique_ptr surface) - : overlay_view(std::move(overlay_view)), - overlay_view_wrapper(std::move(overlay_view_wrapper)), - ios_surface(std::move(ios_surface)), - surface(std::move(surface)){}; + fml::scoped_nsobject overlay_view, + fml::scoped_nsobject overlay_view_wrapper, + std::unique_ptr ios_surface, + std::unique_ptr surface) +: overlay_view(std::move(overlay_view)), +overlay_view_wrapper(std::move(overlay_view_wrapper)), +ios_surface(std::move(ios_surface)), +surface(std::move(surface)){}; FlutterPlatformViewLayer::~FlutterPlatformViewLayer() = default; FlutterPlatformViewsController::FlutterPlatformViewsController() - : layer_pool_(std::make_unique()), - weak_factory_(std::make_unique>(this)){}; +: layer_pool_(std::make_unique()), +weak_factory_(std::make_unique>(this)){}; FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; @@ -40,7 +40,7 @@ CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix& matrix) { transform.m21 = matrix.getSkewX(); transform.m41 = matrix.getTranslateX(); transform.m14 = matrix.getPerspX(); - + transform.m12 = matrix.getSkewY(); transform.m22 = matrix.getScaleY(); transform.m42 = matrix.getTranslateY(); @@ -56,7 +56,9 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@implementation ChildClippingView +@implementation ChildClippingView { + NSObject* _gaussianFilter; +} // 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 @@ -70,6 +72,43 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } +- (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { + if (!_gaussianFilter) { + UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + + UIView* view = [visualEffectView.subviews firstObject]; + if (!view) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter."; + return; + } + + _gaussianFilter = [[view.layer.filters firstObject] retain]; + if (!_gaussianFilter) + return; + + FML_DCHECK([[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]); + [visualEffectView release]; + } + + if(![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter's properties."; + return; + } + + if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) + return; + + [_gaussianFilter setValue:blurRadius + forKey:@"inputRadius"]; + self.layer.filters = @[_gaussianFilter]; +} + +- (void)dealloc { + [_gaussianFilter release]; + _gaussianFilter = nil; + [super dealloc]; +} + @end @interface FlutterClippingMaskView () @@ -102,10 +141,10 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { - (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); @@ -146,7 +185,7 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matri SkVector topRightRadii = clipSkRRect.radii(SkRRect::kUpperRight_Corner); SkVector bottomRightRadii = clipSkRRect.radii(SkRRect::kLowerRight_Corner); SkVector bottomLeftRadii = clipSkRRect.radii(SkRRect::kLowerLeft_Corner); - + // Start drawing RRect // Move point to the top left corner adding the top left radii's x. CGPathMoveToPoint(mutablePathRef, nil, clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); @@ -175,7 +214,7 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matri clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop, clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); CGPathCloseSubpath(mutablePathRef); - + pathRef = mutablePathRef; break; } @@ -194,7 +233,7 @@ - (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { return; } CGMutablePathRef pathRef = CGPathCreateMutable(); - + // Loop through all verbs and translate them into CGPath SkPath::Iter iter(path, true); SkPoint pts[kMaxPointsInVerb]; @@ -250,7 +289,7 @@ - (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { - (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { CGAffineTransform affine = - CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); + CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine); CGPathRelease(path); return fml::CFRef(transformedPath); diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 3632c55063a94..5409ced4bf6c6 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -69,9 +69,9 @@ 0D6AB6C922BB05E200EEE540 /* IosUnitTestsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IosUnitTestsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6CF22BB05E200EEE540 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = ""; }; + 3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = ""; }; 689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = ""; }; 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = ""; }; - 3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = ""; }; F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = ""; }; F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = ""; }; F77E081726FA9CE6003E6E4C /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = "../../../../out/$(FLUTTER_ENGINE)/Flutter.framework"; sourceTree = ""; }; @@ -446,6 +446,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = App/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -466,6 +467,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = App/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", From 50277539ab544cfc0f73115ec8ce0c6dbc606a71 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Fri, 15 Jul 2022 12:16:34 -0700 Subject: [PATCH 02/51] Removed trailing whitespace --- .../framework/Source/FlutterPlatformViews.mm | 1 - .../Source/FlutterPlatformViewsTest.mm | 24 ++++--- .../Source/FlutterPlatformViews_Internal.h | 3 +- .../Source/FlutterPlatformViews_Internal.mm | 62 ++++++++++--------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 6d1c9f66ca746..6a785c3629885 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import #import #include diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 6ca9dc8398fbb..bb248ede3e0ac 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -5,8 +5,6 @@ #import #import #import -#import -#import #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" @@ -418,17 +416,17 @@ - (void)testChildClippingViewApplyDuplicateBackdropFilter { ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; [mockFlutterView addSubview:childClippingView]; - [mockFlutterView setNeedsLayout]; - [mockFlutterView layoutIfNeeded]; - - // childClippingView has the CAFilter, no additional filters were added - XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); - - NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; - XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); - - // No new views were added - XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } - (void)testCompositePlatformView { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index aea92f84ab5af..fa6cc25e554df 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -49,7 +49,8 @@ @interface ChildClippingView : UIView // Adds a blur filter to its layers. -- (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? +- (void)applyBackdropFilterWithRadius: + (NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index a53b831084d6e..6f38f83074536 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -12,20 +12,20 @@ namespace flutter { FlutterPlatformViewLayer::FlutterPlatformViewLayer( - fml::scoped_nsobject overlay_view, - fml::scoped_nsobject overlay_view_wrapper, - std::unique_ptr ios_surface, - std::unique_ptr surface) -: overlay_view(std::move(overlay_view)), -overlay_view_wrapper(std::move(overlay_view_wrapper)), -ios_surface(std::move(ios_surface)), -surface(std::move(surface)){}; + fml::scoped_nsobject overlay_view, + fml::scoped_nsobject overlay_view_wrapper, + std::unique_ptr ios_surface, + std::unique_ptr surface) + : overlay_view(std::move(overlay_view)), + overlay_view_wrapper(std::move(overlay_view_wrapper)), + ios_surface(std::move(ios_surface)), + surface(std::move(surface)){}; FlutterPlatformViewLayer::~FlutterPlatformViewLayer() = default; FlutterPlatformViewsController::FlutterPlatformViewsController() -: layer_pool_(std::make_unique()), -weak_factory_(std::make_unique>(this)){}; + : layer_pool_(std::make_unique()), + weak_factory_(std::make_unique>(this)){}; FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; @@ -40,7 +40,7 @@ CATransform3D GetCATransform3DFromSkMatrix(const SkMatrix& matrix) { transform.m21 = matrix.getSkewX(); transform.m41 = matrix.getTranslateX(); transform.m14 = matrix.getPerspX(); - + transform.m12 = matrix.getSkewY(); transform.m22 = matrix.getScaleY(); transform.m42 = matrix.getTranslateY(); @@ -74,33 +74,35 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { - (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { if (!_gaussianFilter) { - UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - + UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + UIView* view = [visualEffectView.subviews firstObject]; if (!view) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter."; return; } - + _gaussianFilter = [[view.layer.filters firstObject] retain]; if (!_gaussianFilter) return; - + FML_DCHECK([[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]); [visualEffectView release]; } - - if(![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter's properties."; + + if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter's properties."; return; } - + if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) return; - - [_gaussianFilter setValue:blurRadius - forKey:@"inputRadius"]; - self.layer.filters = @[_gaussianFilter]; + + [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; + self.layer.filters = @[ _gaussianFilter ]; } - (void)dealloc { @@ -141,10 +143,10 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { - (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); @@ -185,7 +187,7 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matri SkVector topRightRadii = clipSkRRect.radii(SkRRect::kUpperRight_Corner); SkVector bottomRightRadii = clipSkRRect.radii(SkRRect::kLowerRight_Corner); SkVector bottomLeftRadii = clipSkRRect.radii(SkRRect::kLowerLeft_Corner); - + // Start drawing RRect // Move point to the top left corner adding the top left radii's x. CGPathMoveToPoint(mutablePathRef, nil, clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); @@ -214,7 +216,7 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matri clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop, clipSkRect.fLeft + topLeftRadii.x(), clipSkRect.fTop); CGPathCloseSubpath(mutablePathRef); - + pathRef = mutablePathRef; break; } @@ -233,7 +235,7 @@ - (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { return; } CGMutablePathRef pathRef = CGPathCreateMutable(); - + // Loop through all verbs and translate them into CGPath SkPath::Iter iter(path, true); SkPoint pts[kMaxPointsInVerb]; @@ -289,7 +291,7 @@ - (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix { - (fml::CFRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix { CGAffineTransform affine = - CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); + CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42); CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine); CGPathRelease(path); return fml::CFRef(transformedPath); From 24731fa669af122fe0e565535556fa8408fad2df Mon Sep 17 00:00:00 2001 From: emilyabest Date: Fri, 15 Jul 2022 15:18:51 -0700 Subject: [PATCH 03/51] Removed conflict code. Initial attempt with backdrop filter case. --- DEPS | 4 ---- ci/licenses_golden/licenses_skia | 4 ---- .../darwin/ios/framework/Source/FlutterPlatformViews.mm | 2 +- .../ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/DEPS b/DEPS index bfdceb24f3fbd..5182d8ee36ff3 100644 --- a/DEPS +++ b/DEPS @@ -17,11 +17,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', -<<<<<<< HEAD 'skia_revision': '2910d7bf6c04d951d149dc15af165bb1afe78804', -======= - 'skia_revision': 'c83c3459715c4bfae757f1bad5805f6f6423052a', ->>>>>>> d80dd7bfa3 (# This is a combination of 23 commits.) # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index c7cdebc2fd3de..49ddcf8f360e2 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,8 +1,4 @@ -<<<<<<< HEAD Signature: 86c5103d0a1040cb4fc664d07992329e -======= -Signature: 78b6011f8e217bf28cc67c3f6c492758 ->>>>>>> d80dd7bfa3 (# This is a combination of 23 commits.) UNUSED LICENSES: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 6a785c3629885..fd9ce82639f16 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -409,7 +409,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - [clipView applyBackdropFilterWithRadius:@(5)]; break; } case kClipRect: @@ -425,6 +424,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; case kBackdropFilter: + [clipView applyBackdropFilterWithRadius:@(5)]; break; } ++iter; diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 5409ced4bf6c6..226807b6be13a 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -69,9 +69,9 @@ 0D6AB6C922BB05E200EEE540 /* IosUnitTestsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IosUnitTestsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6CF22BB05E200EEE540 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = ""; }; - 3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = ""; }; 689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = ""; }; 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = ""; }; + 3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = ""; }; F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = ""; }; F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = ""; }; F77E081726FA9CE6003E6E4C /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = "../../../../out/$(FLUTTER_ENGINE)/Flutter.framework"; sourceTree = ""; }; From 83b6607c27126079d5a1d8e28dbbbcccb9d5d5c2 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Mon, 18 Jul 2022 09:47:22 -0700 Subject: [PATCH 04/51] Removed duplicate test --- .../Source/FlutterPlatformViewsTest.mm | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index bb248ede3e0ac..ff94fe0562b99 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -366,69 +366,6 @@ - (void)testChildClippingViewApplyDuplicateBackdropFilter { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } -- (void)testChildClippingViewApplyDuplicateBackdropFilter { - flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; - auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); - flutter::TaskRunners runners(/*label=*/self.name.UTF8String, - /*platform=*/thread_task_runner, - /*raster=*/thread_task_runner, - /*ui=*/thread_task_runner, - /*io=*/thread_task_runner); - auto flutterPlatformViewsController = std::make_shared(); - auto platform_view = std::make_unique( - /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, - /*platform_views_controller=*/flutterPlatformViewsController, - /*task_runners=*/runners); - - 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); // TODO EMILY: should viewType be backdropFilter? - - 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); - stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter - stack.PushTransform(screenScaleMatrix); - - 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]; - - // childClippingView has the CAFilter, no additional filters were added - XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); - - NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; - XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); - - // No new views were added - XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); -} - - (void)testCompositePlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); From fb0684cd3fe11aa5335586642712368afb109d25 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 19 Jul 2022 10:39:04 -0700 Subject: [PATCH 05/51] Reverted code to pass the unit tests. Prepared method calls for merging the next PR. Still need to test new method calls. --- .../framework/Source/FlutterPlatformViews.mm | 8 ++- .../Source/FlutterPlatformViews_Internal.h | 7 +++ .../Source/FlutterPlatformViews_Internal.mm | 50 +++++++++++++++++++ .../IosUnitTests.xcodeproj/project.pbxproj | 2 - 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index fd9ce82639f16..b7553665ec6a7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -409,6 +409,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); + + [clipView applyBackdropFilterWithRadius:@(5)]; break; } case kClipRect: @@ -423,9 +425,11 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kOpacity: embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; - case kBackdropFilter: - [clipView applyBackdropFilterWithRadius:@(5)]; + case kBackdropFilter: { + [clipView applyBackdropFilter:(*iter)->GetFilter()]; // TODO EMILY: check for nullptr here? or just in applyBackdropFilter method? break; + } + } ++iter; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index fa6cc25e554df..d37299bc9bbdd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -15,6 +15,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h" #import "flutter/shell/platform/darwin/ios/ios_context.h" #include "third_party/skia/include/core/SkPictureRecorder.h" +#include "flutter/display_list/display_list_image_filter.h" @class FlutterTouchInterceptingView; @@ -52,6 +53,12 @@ - (void)applyBackdropFilterWithRadius: (NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? +// Adds a blur filter to its layers. +- (void)applyBackdropFilter: + (const flutter::DlImageFilter&)blurFilter; +// TODO EMILY: This method was added for when Javon's code is ready. Replace applyBackdropFilterWithRadius: with applyBackdropFilter: + + @end namespace flutter { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 6f38f83074536..7abf784093cca 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -97,9 +97,59 @@ - (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { "access the Gaussian blur filter's properties."; return; } + + if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) + return; + + [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; + self.layer.filters = @[ _gaussianFilter ]; +} + +// TODO EMILY: This method was added for when Javon's code is ready. Replace applyBackdropFilterWithRadius: with applyBackdropFilter: +- (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { + if (!_gaussianFilter) { + UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + + UIView* view = [visualEffectView.subviews firstObject]; + if (!view) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter."; + return; + } + + _gaussianFilter = [[view.layer.filters firstObject] retain]; + if (!_gaussianFilter) + return; + + FML_DCHECK([[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]); + [visualEffectView release]; + } + + if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter's properties."; + return; + } + + // TODO EMILY: I don't know if this works to get the sigma values. + NSNumber* sigmaX = 0; + NSNumber* sigmaY = 0; + NSNumber* blurRadius = 0; + + if(!blurFilter.asBlur()) { + return; + } else { + sigmaX = @(blurFilter.asBlur()->sigma_x()); + sigmaY = @(blurFilter.asBlur()->sigma_y()); + + // TODO EMILY: Math to calculate blurRadius -> Choose the larger value. + blurRadius = MAX(sigmaX, sigmaY); + } if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) return; + // When we add Javon's code, the backdrop filter case will only be called once. Will this check be needed? To update the radius value, will a new Backdrop layer be read and added to stack? Or will an additional layer be added on top of the original layer? Stacking multiple backdrop layers affects the FlutterView differently (blurred edge along overlay). I think we need to let the BackdropFilter layers stack on PlatformViews to have the same effect, not simply update the radius values. I wonder how the effect will look since the BDFilter layers are added to the view, not on top of the view. [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; self.layer.filters = @[ _gaussianFilter ]; diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 226807b6be13a..3632c55063a94 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -446,7 +446,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = App/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -467,7 +466,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = App/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", From 31567f403aee5d40c424843df2d347039305f03b Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 19 Jul 2022 11:11:26 -0700 Subject: [PATCH 06/51] Ran whitespace check --- .../ios/framework/Source/FlutterPlatformViews.mm | 9 +++++---- .../Source/FlutterPlatformViews_Internal.h | 9 ++++----- .../Source/FlutterPlatformViews_Internal.mm | 15 +++++++++++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b7553665ec6a7..ddc3b4bc1aad6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -409,7 +409,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - + [clipView applyBackdropFilterWithRadius:@(5)]; break; } @@ -425,11 +425,12 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kOpacity: embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; - case kBackdropFilter: { - [clipView applyBackdropFilter:(*iter)->GetFilter()]; // TODO EMILY: check for nullptr here? or just in applyBackdropFilter method? + case kBackdropFilter: { + [clipView + applyBackdropFilter:(*iter)->GetFilter()]; // TODO EMILY: check for nullptr here? or + // just in applyBackdropFilter method? break; } - } ++iter; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index d37299bc9bbdd..5cb5af6d5e0ce 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ +#include "flutter/display_list/display_list_image_filter.h" #include "flutter/flow/embedded_views.h" #include "flutter/flow/rtree.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" @@ -15,7 +16,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h" #import "flutter/shell/platform/darwin/ios/ios_context.h" #include "third_party/skia/include/core/SkPictureRecorder.h" -#include "flutter/display_list/display_list_image_filter.h" @class FlutterTouchInterceptingView; @@ -54,10 +54,9 @@ (NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? // Adds a blur filter to its layers. -- (void)applyBackdropFilter: - (const flutter::DlImageFilter&)blurFilter; -// TODO EMILY: This method was added for when Javon's code is ready. Replace applyBackdropFilterWithRadius: with applyBackdropFilter: - +- (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter; +// TODO EMILY: This method was added for when Javon's code is ready. Replace +// applyBackdropFilterWithRadius: with applyBackdropFilter: @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 7abf784093cca..e0147f5f6127d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -97,7 +97,7 @@ - (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { "access the Gaussian blur filter's properties."; return; } - + if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) return; @@ -105,7 +105,8 @@ - (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { self.layer.filters = @[ _gaussianFilter ]; } -// TODO EMILY: This method was added for when Javon's code is ready. Replace applyBackdropFilterWithRadius: with applyBackdropFilter: +// TODO EMILY: This method was added for when Javon's code is ready. Replace +// applyBackdropFilterWithRadius: with applyBackdropFilter: - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { if (!_gaussianFilter) { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] @@ -137,7 +138,7 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { NSNumber* sigmaY = 0; NSNumber* blurRadius = 0; - if(!blurFilter.asBlur()) { + if (!blurFilter.asBlur()) { return; } else { sigmaX = @(blurFilter.asBlur()->sigma_x()); @@ -149,7 +150,13 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) return; - // When we add Javon's code, the backdrop filter case will only be called once. Will this check be needed? To update the radius value, will a new Backdrop layer be read and added to stack? Or will an additional layer be added on top of the original layer? Stacking multiple backdrop layers affects the FlutterView differently (blurred edge along overlay). I think we need to let the BackdropFilter layers stack on PlatformViews to have the same effect, not simply update the radius values. I wonder how the effect will look since the BDFilter layers are added to the view, not on top of the view. + // When we add Javon's code, the backdrop filter case will only be called once. Will this check be + // needed? To update the radius value, will a new Backdrop layer be read and added to stack? Or + // will an additional layer be added on top of the original layer? Stacking multiple backdrop + // layers affects the FlutterView differently (blurred edge along overlay). I think we need to let + // the BackdropFilter layers stack on PlatformViews to have the same effect, not simply update the + // radius values. I wonder how the effect will look since the BDFilter layers are added to the + // view, not on top of the view. [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; self.layer.filters = @[ _gaussianFilter ]; From 2bca98738dd326b49c5036bdb423e8d9caefc798 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 19 Jul 2022 14:10:33 -0700 Subject: [PATCH 07/51] Bringing back old checks. --- .../ios/framework/Source/FlutterPlatformViews.mm | 1 + .../Source/FlutterPlatformViews_Internal.mm | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index ddc3b4bc1aad6..d8dc11b19609d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -381,6 +381,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view) { + if (flutter_view_ == nullptr) { return; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index e0147f5f6127d..2dcddf0b78644 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -77,18 +77,18 @@ - (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - UIView* view = [visualEffectView.subviews firstObject]; - if (!view) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter."; + UIView* view = [visualEffectView.subviews firstObject]; // check view name is BackdropView + if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; return; } _gaussianFilter = [[view.layer.filters firstObject] retain]; - if (!_gaussianFilter) + if (!_gaussianFilter || ![[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter. "; return; + } - FML_DCHECK([[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]); [visualEffectView release]; } From e830e4a971099b31694297f3dc9abd94e1a267a0 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 19 Jul 2022 16:49:47 -0700 Subject: [PATCH 08/51] Updated code to plug into Javon's PR. Started unit tests for integrated code. --- .../framework/Source/FlutterPlatformViews.mm | 2 +- .../Source/FlutterPlatformViewsTest.mm | 218 ++++++++++++------ .../Source/FlutterPlatformViews_Internal.mm | 88 ++++--- 3 files changed, 184 insertions(+), 124 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index d8dc11b19609d..6b819ed64ed92 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -411,7 +411,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - [clipView applyBackdropFilterWithRadius:@(5)]; +// [clipView applyBackdropFilterWithRadius:@(5)]; break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index ff94fe0562b99..661d272e409ac 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -13,6 +13,8 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" +#import "flutter/display_list/display_list_image_filter.h" +#import "flutter/display_list/display_list_tile_mode.h" FLUTTER_ASSERT_NOT_ARC @class FlutterPlatformViewsTestMockPlatformView; @@ -243,7 +245,130 @@ - (void)testChildClippingViewHitTests { XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); } -- (void)testChildClippingViewApplyBackdropFilter { +//- (void)testChildClippingViewApplyBackdropFilter { +// flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; +// auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); +// flutter::TaskRunners runners(/*label=*/self.name.UTF8String, +// /*platform=*/thread_task_runner, +// /*raster=*/thread_task_runner, +// /*ui=*/thread_task_runner, +// /*io=*/thread_task_runner); +// auto flutterPlatformViewsController = std::make_shared(); +// auto platform_view = std::make_unique( +// /*delegate=*/mock_delegate, +// /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, +// /*platform_views_controller=*/flutterPlatformViewsController, +// /*task_runners=*/runners); +// +// 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); // TODO EMILY: should viewType be backdropFilter? +// +// 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); +// stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter +// +// auto embeddedViewParams = +// std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); +// +// flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); +// flutterPlatformViewsController->CompositeEmbeddedView(2); +// XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); +// ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; +// [mockFlutterView addSubview:childClippingView]; +// +// [mockFlutterView setNeedsLayout]; +// [mockFlutterView layoutIfNeeded]; +// +// // childClippingView has the CAFilter +// XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); +// +// NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; +// XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); +// +// // No new views were added +// XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +//} +// +//- (void)testChildClippingViewApplyDuplicateBackdropFilter { +// flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; +// auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); +// flutter::TaskRunners runners(/*label=*/self.name.UTF8String, +// /*platform=*/thread_task_runner, +// /*raster=*/thread_task_runner, +// /*ui=*/thread_task_runner, +// /*io=*/thread_task_runner); +// auto flutterPlatformViewsController = std::make_shared(); +// auto platform_view = std::make_unique( +// /*delegate=*/mock_delegate, +// /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, +// /*platform_views_controller=*/flutterPlatformViewsController, +// /*task_runners=*/runners); +// +// 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); // TODO EMILY: should viewType be backdropFilter? +// +// 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); +// stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter +// stack.PushTransform(screenScaleMatrix); +// +// auto embeddedViewParams = +// std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); +// +// flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); +// flutterPlatformViewsController->CompositeEmbeddedView(2); +// XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); +// ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; +// [mockFlutterView addSubview:childClippingView]; +// +// [mockFlutterView setNeedsLayout]; +// [mockFlutterView layoutIfNeeded]; +// +// // childClippingView has the CAFilter, no additional filters were added +// XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); +// +// NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; +// XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); +// +// // No new views were added +// XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +//} + +-(void)testReceiveDlImageFilter { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); flutter::TaskRunners runners(/*label=*/self.name.UTF8String, @@ -253,72 +378,11 @@ - (void)testChildClippingViewApplyBackdropFilter { /*io=*/thread_task_runner); auto flutterPlatformViewsController = std::make_shared(); auto platform_view = std::make_unique( - /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, - /*platform_views_controller=*/flutterPlatformViewsController, - /*task_runners=*/runners); - - 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); // TODO EMILY: should viewType be backdropFilter? - - 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); - stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter - - auto embeddedViewParams = - std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); - - flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); - flutterPlatformViewsController->CompositeEmbeddedView(2); - XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); - ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; - [mockFlutterView addSubview:childClippingView]; - - [mockFlutterView setNeedsLayout]; - [mockFlutterView layoutIfNeeded]; - - // childClippingView has the CAFilter - XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); - - NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; - XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); - - // No new views were added - XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); -} - -- (void)testChildClippingViewApplyDuplicateBackdropFilter { - flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; - auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); - flutter::TaskRunners runners(/*label=*/self.name.UTF8String, - /*platform=*/thread_task_runner, - /*raster=*/thread_task_runner, - /*ui=*/thread_task_runner, - /*io=*/thread_task_runner); - auto flutterPlatformViewsController = std::make_shared(); - auto platform_view = std::make_unique( - /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, - /*platform_views_controller=*/flutterPlatformViewsController, - /*task_runners=*/runners); - + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; flutterPlatformViewsController->RegisterViewFactory( @@ -330,8 +394,8 @@ - (void)testChildClippingViewApplyDuplicateBackdropFilter { [FlutterMethodCall methodCallWithMethodName:@"create" arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], - result); // TODO EMILY: should viewType be backdropFilter? - + result); + XCTAssertNotNil(gMockPlatformView); UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; @@ -341,26 +405,30 @@ - (void)testChildClippingViewApplyDuplicateBackdropFilter { // Layer tree always pushes a screen scale factor to the stack SkMatrix screenScaleMatrix = SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); - stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter stack.PushTransform(screenScaleMatrix); - + // Push a backdrop filter + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kClamp); + stack.PushBackdropFilter(filter); + auto embeddedViewParams = std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); - + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); - XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; [mockFlutterView addSubview:childClippingView]; [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - + // childClippingView has the CAFilter, no additional filters were added XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // sigmaX was chosen for input radius NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; - XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 2dcddf0b78644..2ff20d8bcc730 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -72,7 +72,42 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } -- (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { +//- (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { +// if (!_gaussianFilter) { +// UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] +// initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; +// +// UIView* view = [visualEffectView.subviews firstObject]; // check view name is BackdropView +// if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { +// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; +// return; +// } +// +// _gaussianFilter = [[view.layer.filters firstObject] retain]; +// if (!_gaussianFilter || ![[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { +// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter. "; +// return; +// } +// +// [visualEffectView release]; +// } +// +// if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that inputRadius key is valid? +// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " +// "access the Gaussian blur filter's properties."; +// return; +// } +// +// if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) +// return; +// +// [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; +// self.layer.filters = @[ _gaussianFilter ]; +//} + +// TODO EMILY: This method was added for when Javon's code is ready. Replace +// applyBackdropFilterWithRadius: with applyBackdropFilter: +- (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { if (!_gaussianFilter) { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; @@ -92,61 +127,18 @@ - (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { [visualEffectView release]; } - if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter's properties."; - return; - } - - if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) - return; - - [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; - self.layer.filters = @[ _gaussianFilter ]; -} - -// TODO EMILY: This method was added for when Javon's code is ready. Replace -// applyBackdropFilterWithRadius: with applyBackdropFilter: -- (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { - if (!_gaussianFilter) { - UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - - UIView* view = [visualEffectView.subviews firstObject]; - if (!view) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter."; - return; - } - - _gaussianFilter = [[view.layer.filters firstObject] retain]; - if (!_gaussianFilter) - return; - - FML_DCHECK([[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]); - [visualEffectView release]; - } - - if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { + if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that inputRadius key is valid? FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter's properties."; return; } - // TODO EMILY: I don't know if this works to get the sigma values. - NSNumber* sigmaX = 0; - NSNumber* sigmaY = 0; - NSNumber* blurRadius = 0; - if (!blurFilter.asBlur()) { + FML_DLOG(ERROR) << "The backdrop filter was not added correctly."; return; - } else { - sigmaX = @(blurFilter.asBlur()->sigma_x()); - sigmaY = @(blurFilter.asBlur()->sigma_y()); - - // TODO EMILY: Math to calculate blurRadius -> Choose the larger value. - blurRadius = MAX(sigmaX, sigmaY); } + + NSNumber* blurRadius = @(blurFilter.asBlur()->sigma_x()); if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) return; From c881e1193ab73a21dc1d7db8fa7ecb449fb58a85 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 20 Jul 2022 15:34:36 -0700 Subject: [PATCH 09/51] Saving applyBackdropFilterWithRadius method for reference. --- .../ios/framework/Source/FlutterPlatformViews.mm | 10 ++++++++++ .../framework/Source/FlutterPlatformViews_Internal.h | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 6b819ed64ed92..b477a36dab516 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -412,6 +412,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { finalTransform = CATransform3DConcat(transform, finalTransform); // [clipView applyBackdropFilterWithRadius:@(5)]; + +// [clipView +// applyBackdropFilter:(*iter)->GetFilter()]; + + // TODO EMILY: these lines are for visual tests, delete before landing PR + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); + + [clipView + applyBackdropFilter:filter]; + break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 5cb5af6d5e0ce..87100e6f7fef6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -50,8 +50,8 @@ @interface ChildClippingView : UIView // Adds a blur filter to its layers. -- (void)applyBackdropFilterWithRadius: - (NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? +//- (void)applyBackdropFilterWithRadius: +// (NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? // Adds a blur filter to its layers. - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter; From 81d2d7194cbd912e5c5e97009b12cdd3ec198077 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 20 Jul 2022 15:37:13 -0700 Subject: [PATCH 10/51] Removed applyBackdropFilterWithRadius method. This is the new starting point. --- .../framework/Source/FlutterPlatformViews.mm | 5 --- .../Source/FlutterPlatformViews_Internal.h | 6 ---- .../Source/FlutterPlatformViews_Internal.mm | 35 ------------------- 3 files changed, 46 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b477a36dab516..2cb1235232c39 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -410,11 +410,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - -// [clipView applyBackdropFilterWithRadius:@(5)]; - -// [clipView -// applyBackdropFilter:(*iter)->GetFilter()]; // TODO EMILY: these lines are for visual tests, delete before landing PR flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 87100e6f7fef6..d4f86da75a736 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -49,14 +49,8 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -// Adds a blur filter to its layers. -//- (void)applyBackdropFilterWithRadius: -// (NSNumber*)blurRadius; // TODO EMILY: is it better to pass an int? - // Adds a blur filter to its layers. - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter; -// TODO EMILY: This method was added for when Javon's code is ready. Replace -// applyBackdropFilterWithRadius: with applyBackdropFilter: @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 2ff20d8bcc730..6729fae8d3dec 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -72,41 +72,6 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } -//- (void)applyBackdropFilterWithRadius:(NSNumber*)blurRadius { -// if (!_gaussianFilter) { -// UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] -// initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; -// -// UIView* view = [visualEffectView.subviews firstObject]; // check view name is BackdropView -// if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { -// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; -// return; -// } -// -// _gaussianFilter = [[view.layer.filters firstObject] retain]; -// if (!_gaussianFilter || ![[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { -// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter. "; -// return; -// } -// -// [visualEffectView release]; -// } -// -// if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that inputRadius key is valid? -// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " -// "access the Gaussian blur filter's properties."; -// return; -// } -// -// if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) -// return; -// -// [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; -// self.layer.filters = @[ _gaussianFilter ]; -//} - -// TODO EMILY: This method was added for when Javon's code is ready. Replace -// applyBackdropFilterWithRadius: with applyBackdropFilter: - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { if (!_gaussianFilter) { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] From bc2e1b1f3ca4fe33252b2cd06e7311624dd3de07 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 20 Jul 2022 17:19:00 -0700 Subject: [PATCH 11/51] Updated unit tests to use applyBackdropFilter method --- .../Source/FlutterPlatformViewsTest.mm | 197 +++++++----------- .../Source/FlutterPlatformViews_Internal.mm | 5 +- 2 files changed, 75 insertions(+), 127 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 661d272e409ac..13659ab2490d1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -13,8 +13,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" -#import "flutter/display_list/display_list_image_filter.h" -#import "flutter/display_list/display_list_tile_mode.h" FLUTTER_ASSERT_NOT_ARC @class FlutterPlatformViewsTestMockPlatformView; @@ -245,130 +243,73 @@ - (void)testChildClippingViewHitTests { XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); } -//- (void)testChildClippingViewApplyBackdropFilter { -// flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; -// auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); -// flutter::TaskRunners runners(/*label=*/self.name.UTF8String, -// /*platform=*/thread_task_runner, -// /*raster=*/thread_task_runner, -// /*ui=*/thread_task_runner, -// /*io=*/thread_task_runner); -// auto flutterPlatformViewsController = std::make_shared(); -// auto platform_view = std::make_unique( -// /*delegate=*/mock_delegate, -// /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, -// /*platform_views_controller=*/flutterPlatformViewsController, -// /*task_runners=*/runners); -// -// 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); // TODO EMILY: should viewType be backdropFilter? -// -// 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); -// stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter -// -// auto embeddedViewParams = -// std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); -// -// flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); -// flutterPlatformViewsController->CompositeEmbeddedView(2); -// XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); -// ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; -// [mockFlutterView addSubview:childClippingView]; -// -// [mockFlutterView setNeedsLayout]; -// [mockFlutterView layoutIfNeeded]; -// -// // childClippingView has the CAFilter -// XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); -// -// NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; -// XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); -// -// // No new views were added -// XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); -//} -// -//- (void)testChildClippingViewApplyDuplicateBackdropFilter { -// flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; -// auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); -// flutter::TaskRunners runners(/*label=*/self.name.UTF8String, -// /*platform=*/thread_task_runner, -// /*raster=*/thread_task_runner, -// /*ui=*/thread_task_runner, -// /*io=*/thread_task_runner); -// auto flutterPlatformViewsController = std::make_shared(); -// auto platform_view = std::make_unique( -// /*delegate=*/mock_delegate, -// /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, -// /*platform_views_controller=*/flutterPlatformViewsController, -// /*task_runners=*/runners); -// -// 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); // TODO EMILY: should viewType be backdropFilter? -// -// 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); -// stack.PushTransform(screenScaleMatrix); // TODO EMILY: change this to PushBackdropFilter -// stack.PushTransform(screenScaleMatrix); -// -// auto embeddedViewParams = -// std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); -// -// flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); -// flutterPlatformViewsController->CompositeEmbeddedView(2); -// XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); -// ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; -// [mockFlutterView addSubview:childClippingView]; -// -// [mockFlutterView setNeedsLayout]; -// [mockFlutterView layoutIfNeeded]; -// -// // childClippingView has the CAFilter, no additional filters were added -// XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); -// -// NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; -// XCTAssertEqual(@(5), [gaussianFilter valueForKey:@"inputRadius"]); -// -// // No new views were added -// XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); -//} +-(void)testApplyBackdropFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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); --(void)testReceiveDlImageFilter { + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a backdrop filter + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); // TODO EMILY: Should this be tested in a separate unit test? + stack.PushBackdropFilter(filter); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + + // sigmaX is chosen for input radius, regardless of sigmaY + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + +-(void)testDuplicateApplyBackdropFilter { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); flutter::TaskRunners runners(/*label=*/self.name.UTF8String, @@ -409,6 +350,7 @@ -(void)testReceiveDlImageFilter { // Push a backdrop filter flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); + stack.PushBackdropFilter(filter); auto embeddedViewParams = std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); @@ -434,6 +376,11 @@ -(void)testReceiveDlImageFilter { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } +// TODO EMILY: check on how to test this before integration testing +//- (void)testAddingMultipleBackdropFilters { +// +//} + - (void)testCompositePlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 6729fae8d3dec..3cd89777529ab 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -77,7 +77,7 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - UIView* view = [visualEffectView.subviews firstObject]; // check view name is BackdropView + UIView* view = [visualEffectView.subviews firstObject]; if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; return; @@ -92,7 +92,7 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { [visualEffectView release]; } - if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that inputRadius key is valid? + if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that inputRadius key is valid? -> DOCUMENT ATTEMPTS FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter's properties."; return; @@ -103,6 +103,7 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { return; } + // sigma_x is arbitrarily chosen as the blur radius NSNumber* blurRadius = @(blurFilter.asBlur()->sigma_x()); if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) From ff81803c76bd9c154aac4848da7ccb7e86b38770 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 20 Jul 2022 17:23:17 -0700 Subject: [PATCH 12/51] Ran clang checks --- .../framework/Source/FlutterPlatformViews.mm | 11 +++-- .../Source/FlutterPlatformViewsTest.mm | 44 ++++++++++--------- .../Source/FlutterPlatformViews_Internal.mm | 12 +++-- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 2cb1235232c39..03c9b42017476 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -381,7 +381,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view) { - if (flutter_view_ == nullptr) { return; } @@ -410,12 +409,12 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - + // TODO EMILY: these lines are for visual tests, delete before landing PR - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); - - [clipView - applyBackdropFilter:filter]; + flutter::DlBlurImageFilter filter = + flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); + + [clipView applyBackdropFilter:filter]; break; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 13659ab2490d1..ed9ddf0ddd277 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -243,7 +243,7 @@ - (void)testChildClippingViewHitTests { XCTAssertTrue([childClippingView pointInside:CGPointMake(199, 199) withEvent:nil]); } --(void)testApplyBackdropFilter { +- (void)testApplyBackdropFilter { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); flutter::TaskRunners runners(/*label=*/self.name.UTF8String, @@ -253,11 +253,11 @@ -(void)testApplyBackdropFilter { /*io=*/thread_task_runner); auto flutterPlatformViewsController = std::make_shared(); auto platform_view = std::make_unique( - /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, - /*platform_views_controller=*/flutterPlatformViewsController, - /*task_runners=*/runners); - + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; flutterPlatformViewsController->RegisterViewFactory( @@ -270,7 +270,7 @@ -(void)testApplyBackdropFilter { methodCallWithMethodName:@"create" arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], result); - + XCTAssertNotNil(gMockPlatformView); UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; @@ -282,12 +282,14 @@ -(void)testApplyBackdropFilter { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push a backdrop filter - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); // TODO EMILY: Should this be tested in a separate unit test? + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter( + 5, 2, + flutter::DlTileMode::kClamp); // TODO EMILY: Should this be tested in a separate unit test? stack.PushBackdropFilter(filter); - + auto embeddedViewParams = std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); - + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); @@ -296,7 +298,7 @@ -(void)testApplyBackdropFilter { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - + // childClippingView has the CAFilter, no additional filters were added XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); @@ -309,7 +311,7 @@ -(void)testApplyBackdropFilter { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } --(void)testDuplicateApplyBackdropFilter { +- (void)testDuplicateApplyBackdropFilter { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); flutter::TaskRunners runners(/*label=*/self.name.UTF8String, @@ -319,11 +321,11 @@ -(void)testDuplicateApplyBackdropFilter { /*io=*/thread_task_runner); auto flutterPlatformViewsController = std::make_shared(); auto platform_view = std::make_unique( - /*delegate=*/mock_delegate, - /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, - /*platform_views_controller=*/flutterPlatformViewsController, - /*task_runners=*/runners); - + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + FlutterPlatformViewsTestMockFlutterPlatformFactory* factory = [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease]; flutterPlatformViewsController->RegisterViewFactory( @@ -336,7 +338,7 @@ -(void)testDuplicateApplyBackdropFilter { methodCallWithMethodName:@"create" arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], result); - + XCTAssertNotNil(gMockPlatformView); UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; @@ -351,10 +353,10 @@ -(void)testDuplicateApplyBackdropFilter { flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); stack.PushBackdropFilter(filter); - + auto embeddedViewParams = std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); - + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); @@ -363,7 +365,7 @@ -(void)testDuplicateApplyBackdropFilter { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - + // childClippingView has the CAFilter, no additional filters were added XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 3cd89777529ab..6c06ec339837a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -79,20 +79,24 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { UIView* view = [visualEffectView.subviews firstObject]; if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access its subviews."; return; } _gaussianFilter = [[view.layer.filters firstObject] retain]; if (!_gaussianFilter || ![[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter. "; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter. "; return; } [visualEffectView release]; } - if (![[_gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that inputRadius key is valid? -> DOCUMENT ATTEMPTS + if (![[_gaussianFilter valueForKey:@"inputRadius"] + isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that + // inputRadius key is valid? -> DOCUMENT ATTEMPTS FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter's properties."; return; @@ -102,7 +106,7 @@ - (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { FML_DLOG(ERROR) << "The backdrop filter was not added correctly."; return; } - + // sigma_x is arbitrarily chosen as the blur radius NSNumber* blurRadius = @(blurFilter.asBlur()->sigma_x()); From 359a10dc3efdd53b43b631791840b362703d4229 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 21 Jul 2022 16:17:25 -0700 Subject: [PATCH 13/51] First implementation to allow multiple backdrop filters. Also reorganized code. --- .../framework/Source/FlutterPlatformViews.mm | 42 ++++++--- .../Source/FlutterPlatformViews_Internal.h | 2 +- .../Source/FlutterPlatformViews_Internal.mm | 89 ++++++++++--------- 3 files changed, 80 insertions(+), 53 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 03c9b42017476..c00c96ec5ccda 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -402,19 +402,28 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { initWithFrame:CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y, CGRectGetWidth(flutter_view.bounds), CGRectGetHeight(flutter_view.bounds))] autorelease]; - + + NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the best option? + + int numFilters = 0; + auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - - // TODO EMILY: these lines are for visual tests, delete before landing PR - flutter::DlBlurImageFilter filter = - flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); - - [clipView applyBackdropFilter:filter]; + + if(numFilters < 4) { + // TODO EMILY: these lines are for visual tests, delete before landing PR + flutter::DlBlurImageFilter filter = + flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); + + NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + [blurRadii addObject:blurRadius]; + + numFilters++; + } break; } @@ -431,14 +440,27 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; case kBackdropFilter: { - [clipView - applyBackdropFilter:(*iter)->GetFilter()]; // TODO EMILY: check for nullptr here? or - // just in applyBackdropFilter method? + // TODO EMILY: think of a way to avoid repeatedly adding backdropFilters when no changes were made to BDFilters + + if (!(*iter)->GetFilter().asBlur()) { + // We only support DlBlurImageFilter for BackdropFilter. + continue; + } + + // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D rendering. + // DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it is not used to blur the PlatformView. + NSNumber* blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); + [blurRadii addObject:blurRadius]; break; } } ++iter; } + + [clipView applyBackdropFilters:blurRadii]; // TODO EMILY: pass pointer/reference? + + + // 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. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index d4f86da75a736..719db3741dabf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -50,7 +50,7 @@ @interface ChildClippingView : UIView // Adds a blur filter to its layers. -- (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter; +- (void)applyBackdropFilters:(NSArray*)blurRadii; // TODO EMILY: make param NSMutableArray? @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 6c06ec339837a..bcae2cfc2821b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -57,7 +57,7 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter @implementation ChildClippingView { - NSObject* _gaussianFilter; + NSMutableArray* _gaussianFilters;// = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the right thing here? } // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to @@ -72,61 +72,66 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } -- (void)applyBackdropFilter:(const flutter::DlImageFilter&)blurFilter { - if (!_gaussianFilter) { - UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - - UIView* view = [visualEffectView.subviews firstObject]; - if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access its subviews."; +- (void)applyBackdropFilters:(NSArray*)blurRadii { + if(!_gaussianFilters || ([blurRadii count] != [_gaussianFilters count])) { + NSObject* gaussianFilter = [self extractGaussianFilter]; + if(!gaussianFilter) return; + + _gaussianFilters = [NSMutableArray arrayWithCapacity:[blurRadii count]]; + for(NSNumber* radius in blurRadii) { + NSObject* newGaussianFilter = [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to make a new copy for every gaussianFilter? + [newGaussianFilter setValue:radius forKey:@"inputRadius"]; + [_gaussianFilters addObject:newGaussianFilter]; } - - _gaussianFilter = [[view.layer.filters firstObject] retain]; - if (!_gaussianFilter || ![[_gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter. "; - return; + } else { + for(int i = 0; i < (int)[blurRadii count]; i++) { + if([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) + continue; + + [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } + } + + self.layer.filters = _gaussianFilters; +} - [visualEffectView release]; +// Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its gaussianFilter. +// Logs errors and returns if Apple's API has changed and the filter can't be extracted. +- (NSObject*)extractGaussianFilter { + UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + + UIView* view = [visualEffectView.subviews firstObject]; + if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access its subviews."; + return nil; + } + + NSObject* gaussianFilter = [[view.layer.filters firstObject] retain]; + if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter. "; + return nil; } + + [visualEffectView release]; - if (![[_gaussianFilter valueForKey:@"inputRadius"] + if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that // inputRadius key is valid? -> DOCUMENT ATTEMPTS FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter's properties."; - return; - } - - if (!blurFilter.asBlur()) { - FML_DLOG(ERROR) << "The backdrop filter was not added correctly."; - return; + return nil; } - - // sigma_x is arbitrarily chosen as the blur radius - NSNumber* blurRadius = @(blurFilter.asBlur()->sigma_x()); - - if ([[_gaussianFilter valueForKey:@"inputRadius"] isEqual:blurRadius]) - return; - // When we add Javon's code, the backdrop filter case will only be called once. Will this check be - // needed? To update the radius value, will a new Backdrop layer be read and added to stack? Or - // will an additional layer be added on top of the original layer? Stacking multiple backdrop - // layers affects the FlutterView differently (blurred edge along overlay). I think we need to let - // the BackdropFilter layers stack on PlatformViews to have the same effect, not simply update the - // radius values. I wonder how the effect will look since the BDFilter layers are added to the - // view, not on top of the view. - - [_gaussianFilter setValue:blurRadius forKey:@"inputRadius"]; - self.layer.filters = @[ _gaussianFilter ]; + + return gaussianFilter; } - (void)dealloc { - [_gaussianFilter release]; - _gaussianFilter = nil; + [_gaussianFilters release]; + _gaussianFilters = nil; [super dealloc]; } From cfd473d81b69611787a7faab3f9d61ab07baebca Mon Sep 17 00:00:00 2001 From: emilyabest Date: Fri, 22 Jul 2022 16:47:19 -0700 Subject: [PATCH 14/51] Fixed BAD ACCESS error with private instance variable. Prepped for github push. --- .../framework/Source/FlutterPlatformViews.mm | 32 ++- .../Source/FlutterPlatformViewsTest.mm | 219 ++++++++++++++++-- .../Source/FlutterPlatformViews_Internal.h | 2 +- .../Source/FlutterPlatformViews_Internal.mm | 63 ++--- 4 files changed, 255 insertions(+), 61 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index c00c96ec5ccda..5879d23bd17cb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -405,7 +405,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the best option? - int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -414,16 +414,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - if(numFilters < 4) { - // TODO EMILY: these lines are for visual tests, delete before landing PR - flutter::DlBlurImageFilter filter = - flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); - - NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - [blurRadii addObject:blurRadius]; - - numFilters++; - } +// if(numFilters < 1) { +// // TODO EMILY: these lines are for visual tests, delete before landing PR +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// } break; } @@ -440,12 +440,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha; break; case kBackdropFilter: { - // TODO EMILY: think of a way to avoid repeatedly adding backdropFilters when no changes were made to BDFilters - - if (!(*iter)->GetFilter().asBlur()) { - // We only support DlBlurImageFilter for BackdropFilter. - continue; - } + // We only support DlBlurImageFilter for BackdropFilter. + if (!(*iter)->GetFilter().asBlur()) continue; // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D rendering. // DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it is not used to blur the PlatformView. @@ -459,8 +455,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { [clipView applyBackdropFilters:blurRadii]; // TODO EMILY: pass pointer/reference? - - // 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. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index ed9ddf0ddd277..0b0fbf365f531 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -311,7 +311,7 @@ - (void)testApplyBackdropFilter { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } -- (void)testDuplicateApplyBackdropFilter { +- (void)testApplyMultipleBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); flutter::TaskRunners runners(/*label=*/self.name.UTF8String, @@ -349,10 +349,11 @@ - (void)testDuplicateApplyBackdropFilter { SkMatrix screenScaleMatrix = SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); - // Push a backdrop filter - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kClamp); - stack.PushBackdropFilter(filter); - stack.PushBackdropFilter(filter); + // Push backdrop filters + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + for(int i = 0; i < 50; i++) { + stack.PushBackdropFilter(filter); + } auto embeddedViewParams = std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); @@ -366,22 +367,210 @@ - (void)testDuplicateApplyBackdropFilter { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - // childClippingView has the CAFilter, no additional filters were added - XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(50, (int)[childClippingView.layer.filters count]); + + // All filters have sigma X radius + for(int i = 0; i < 50; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } - // sigmaX was chosen for input radius - NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; - XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); - XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} + +//TODO: If a Flutter user removes the backdrop filter, a new stack is created. To simulate this removal in the test, I created a new stack. It doesn't test the removal's effect fully. I think we need to run further integration tests for full testing. +- (void)testRemoveBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push backdrop filters + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + for(int i = 0; i < 5; i++) { + stack.PushBackdropFilter(filter); + } + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Simulate removing 1 backdrop filter (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters + for(int i = 0; i < 4; i++) { + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + + // All filters have sigma X radius + for(int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } -// TODO EMILY: check on how to test this before integration testing -//- (void)testAddingMultipleBackdropFilters { -// -//} +// TODO: Similarly, to simulate a Flutter user editing the input radius of an applied Backdrop Filter, this test creates a new mutators stack. We might need more integration testing to fully test the functionality of editing a Backdrop Filter. +- (void)testEditBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push backdrop filters + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + for(int i = 0; i < 5; i++) { + stack.PushBackdropFilter(filter); + } + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Simulate editing 1 backdrop filter (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters + for(int i = 0; i < 4; i++) { + if(i == 3) { + flutter::DlBlurImageFilter filter2 = flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); // TODO EMILY: is filter actually a reference? Should we be concerned about that? + stack2.PushBackdropFilter(filter2); + continue; + } + + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + + // One filter has radius 2, others have original sigma x radius + int countRadius2 = 0; + int countRadius5 = 0; + + for(int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if([@(5) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) countRadius5++; + else if([@(2) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) countRadius2++; + } + + XCTAssertEqual(1, countRadius2); + XCTAssertEqual(3, countRadius5); + + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); +} - (void)testCompositePlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 719db3741dabf..e9724369f2516 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -50,7 +50,7 @@ @interface ChildClippingView : UIView // Adds a blur filter to its layers. -- (void)applyBackdropFilters:(NSArray*)blurRadii; // TODO EMILY: make param NSMutableArray? +- (void)applyBackdropFilters:(NSArray*)blurRadii; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index bcae2cfc2821b..31be43a913b53 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -56,8 +56,9 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@implementation ChildClippingView { - NSMutableArray* _gaussianFilters;// = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the right thing here? +@implementation ChildClippingView +{ + NSMutableArray* _gaussianFilters; } // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to @@ -72,30 +73,6 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } -- (void)applyBackdropFilters:(NSArray*)blurRadii { - if(!_gaussianFilters || ([blurRadii count] != [_gaussianFilters count])) { - NSObject* gaussianFilter = [self extractGaussianFilter]; - if(!gaussianFilter) - return; - - _gaussianFilters = [NSMutableArray arrayWithCapacity:[blurRadii count]]; - for(NSNumber* radius in blurRadii) { - NSObject* newGaussianFilter = [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to make a new copy for every gaussianFilter? - [newGaussianFilter setValue:radius forKey:@"inputRadius"]; - [_gaussianFilters addObject:newGaussianFilter]; - } - } else { - for(int i = 0; i < (int)[blurRadii count]; i++) { - if([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) - continue; - - [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; - } - } - - self.layer.filters = _gaussianFilters; -} - // Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its gaussianFilter. // Logs errors and returns if Apple's API has changed and the filter can't be extracted. - (NSObject*)extractGaussianFilter { @@ -129,6 +106,40 @@ - (NSObject*)extractGaussianFilter { return gaussianFilter; } +- (void)applyBackdropFilters:(NSArray*)blurRadii { + if(!_gaussianFilters) { + NSObject* gaussianFilter = [self extractGaussianFilter]; + if(!gaussianFilter) return; + + _gaussianFilters = [[[NSMutableArray alloc] init] retain]; // TODO EMILY: does this need retain? + for(NSNumber* radius in blurRadii) { + NSObject* newGaussianFilter = [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to make a new copy for every gaussianFilter? + [newGaussianFilter setValue:radius forKey:@"inputRadius"]; + [_gaussianFilters addObject:newGaussianFilter]; + } + } + else if ([blurRadii count] != [_gaussianFilters count]) { + NSObject* gaussianFilter = [self extractGaussianFilter]; + if(!gaussianFilter) return; + + [_gaussianFilters removeAllObjects]; + for(NSNumber* radius in blurRadii) { + NSObject* newGaussianFilter = [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to make a new copy for every gaussianFilter? + [newGaussianFilter setValue:radius forKey:@"inputRadius"]; + [_gaussianFilters addObject:newGaussianFilter]; + } + } + else { + for(int i = 0; i < (int)[blurRadii count]; i++) { + if([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) continue; + + [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; + } + } + + self.layer.filters = _gaussianFilters; +} + - (void)dealloc { [_gaussianFilters release]; _gaussianFilters = nil; From ac3d4cf177bff979cdd7f8a89b5b73ff1dcdbdb1 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Fri, 22 Jul 2022 16:53:48 -0700 Subject: [PATCH 15/51] Ran formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 49 +++++++------- .../Source/FlutterPlatformViewsTest.mm | 64 +++++++++++-------- .../Source/FlutterPlatformViews_Internal.mm | 64 ++++++++++--------- 3 files changed, 97 insertions(+), 80 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 5879d23bd17cb..02c9c30c481a2 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -402,28 +402,29 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { initWithFrame:CGRectMake(-clipView.frame.origin.x, -clipView.frame.origin.y, CGRectGetWidth(flutter_view.bounds), CGRectGetHeight(flutter_view.bounds))] autorelease]; - - NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the best option? - -// int numFilters = 0; - + + NSMutableArray* blurRadii = + [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the best option? + + // int numFilters = 0; + auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - -// if(numFilters < 1) { -// // TODO EMILY: these lines are for visual tests, delete before landing PR -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// } + + // if(numFilters < 1) { + // // TODO EMILY: these lines are for visual tests, delete before landing PR + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // } break; } @@ -441,10 +442,12 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. - if (!(*iter)->GetFilter().asBlur()) continue; - - // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D rendering. - // DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it is not used to blur the PlatformView. + if (!(*iter)->GetFilter().asBlur()) + continue; + + // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D + // rendering. DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it + // is not used to blur the PlatformView. NSNumber* blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); [blurRadii addObject:blurRadius]; break; @@ -452,9 +455,9 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } ++iter; } - - [clipView applyBackdropFilters:blurRadii]; // TODO EMILY: pass pointer/reference? - + + [clipView applyBackdropFilters:blurRadii]; // TODO EMILY: pass pointer/reference? + // 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. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 0b0fbf365f531..f4cd8302e1da8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -351,7 +351,7 @@ - (void)testApplyMultipleBackdropFilters { stack.PushTransform(screenScaleMatrix); // Push backdrop filters flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); - for(int i = 0; i < 50; i++) { + for (int i = 0; i < 50; i++) { stack.PushBackdropFilter(filter); } @@ -369,9 +369,9 @@ - (void)testApplyMultipleBackdropFilters { // childClippingView has CAFilters for the multiple backdrop filters XCTAssertEqual(50, (int)[childClippingView.layer.filters count]); - + // All filters have sigma X radius - for(int i = 0; i < 50; i++) { + for (int i = 0; i < 50; i++) { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); @@ -381,7 +381,9 @@ - (void)testApplyMultipleBackdropFilters { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } -//TODO: If a Flutter user removes the backdrop filter, a new stack is created. To simulate this removal in the test, I created a new stack. It doesn't test the removal's effect fully. I think we need to run further integration tests for full testing. +// TODO: If a Flutter user removes the backdrop filter, a new stack is created. To simulate this +// removal in the test, I created a new stack. It doesn't test the removal's effect fully. I think +// we need to run further integration tests for full testing. - (void)testRemoveBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); @@ -422,7 +424,7 @@ - (void)testRemoveBackdropFilters { stack.PushTransform(screenScaleMatrix); // Push backdrop filters flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); - for(int i = 0; i < 5; i++) { + for (int i = 0; i < 5; i++) { stack.PushBackdropFilter(filter); } @@ -437,20 +439,20 @@ - (void)testRemoveBackdropFilters { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - + // Simulate removing 1 backdrop filter (create a new mutators stack) // Create embedded view params flutter::MutatorsStack stack2; // Layer tree always pushes a screen scale factor to the stack stack2.PushTransform(screenScaleMatrix); // Push backdrop filters - for(int i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { stack2.PushBackdropFilter(filter); } - - embeddedViewParams = - std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack2); - + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); [mockFlutterView setNeedsLayout]; @@ -460,7 +462,7 @@ - (void)testRemoveBackdropFilters { XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); // All filters have sigma X radius - for(int i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); @@ -470,7 +472,9 @@ - (void)testRemoveBackdropFilters { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } -// TODO: Similarly, to simulate a Flutter user editing the input radius of an applied Backdrop Filter, this test creates a new mutators stack. We might need more integration testing to fully test the functionality of editing a Backdrop Filter. +// TODO: Similarly, to simulate a Flutter user editing the input radius of an applied Backdrop +// Filter, this test creates a new mutators stack. We might need more integration testing to fully +// test the functionality of editing a Backdrop Filter. - (void)testEditBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); @@ -511,7 +515,7 @@ - (void)testEditBackdropFilters { stack.PushTransform(screenScaleMatrix); // Push backdrop filters flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); - for(int i = 0; i < 5; i++) { + for (int i = 0; i < 5; i++) { stack.PushBackdropFilter(filter); } @@ -526,26 +530,28 @@ - (void)testEditBackdropFilters { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - + // Simulate editing 1 backdrop filter (create a new mutators stack) // Create embedded view params flutter::MutatorsStack stack2; // Layer tree always pushes a screen scale factor to the stack stack2.PushTransform(screenScaleMatrix); // Push backdrop filters - for(int i = 0; i < 4; i++) { - if(i == 3) { - flutter::DlBlurImageFilter filter2 = flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); // TODO EMILY: is filter actually a reference? Should we be concerned about that? + for (int i = 0; i < 4; i++) { + if (i == 3) { + flutter::DlBlurImageFilter filter2 = flutter::DlBlurImageFilter( + 2, 5, flutter::DlTileMode::kClamp); // TODO EMILY: is filter actually a reference? Should + // we be concerned about that? stack2.PushBackdropFilter(filter2); continue; } - + stack2.PushBackdropFilter(filter); } - - embeddedViewParams = - std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack2); - + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); [mockFlutterView setNeedsLayout]; @@ -553,18 +559,20 @@ - (void)testEditBackdropFilters { // childClippingView has CAFilters for the multiple backdrop filters XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); - + // One filter has radius 2, others have original sigma x radius int countRadius2 = 0; int countRadius5 = 0; - for(int i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); - if([@(5) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) countRadius5++; - else if([@(2) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) countRadius2++; + if ([@(5) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) + countRadius5++; + else if ([@(2) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) + countRadius2++; } - + XCTAssertEqual(1, countRadius2); XCTAssertEqual(3, countRadius5); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 31be43a913b53..bb5ed1d2bce62 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -56,8 +56,7 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@implementation ChildClippingView -{ +@implementation ChildClippingView { NSMutableArray* _gaussianFilters; } @@ -73,26 +72,27 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { return NO; } -// Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its gaussianFilter. -// Logs errors and returns if Apple's API has changed and the filter can't be extracted. +// Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its +// gaussianFilter. Logs errors and returns if Apple's API has changed and the filter can't be +// extracted. - (NSObject*)extractGaussianFilter { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + UIView* view = [visualEffectView.subviews firstObject]; if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access its subviews."; + "access its subviews."; return nil; } - + NSObject* gaussianFilter = [[view.layer.filters firstObject] retain]; if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter. "; + "access the Gaussian blur filter. "; return nil; } - + [visualEffectView release]; if (![[gaussianFilter valueForKey:@"inputRadius"] @@ -102,41 +102,47 @@ - (NSObject*)extractGaussianFilter { "access the Gaussian blur filter's properties."; return nil; } - + return gaussianFilter; } - (void)applyBackdropFilters:(NSArray*)blurRadii { - if(!_gaussianFilters) { + if (!_gaussianFilters) { NSObject* gaussianFilter = [self extractGaussianFilter]; - if(!gaussianFilter) return; - - _gaussianFilters = [[[NSMutableArray alloc] init] retain]; // TODO EMILY: does this need retain? - for(NSNumber* radius in blurRadii) { - NSObject* newGaussianFilter = [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to make a new copy for every gaussianFilter? + if (!gaussianFilter) + return; + + _gaussianFilters = + [[[NSMutableArray alloc] init] retain]; // TODO EMILY: does this need retain? + for (NSNumber* radius in blurRadii) { + NSObject* newGaussianFilter = + [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to + // make a new copy for every gaussianFilter? [newGaussianFilter setValue:radius forKey:@"inputRadius"]; [_gaussianFilters addObject:newGaussianFilter]; } - } - else if ([blurRadii count] != [_gaussianFilters count]) { + } else if ([blurRadii count] != [_gaussianFilters count]) { NSObject* gaussianFilter = [self extractGaussianFilter]; - if(!gaussianFilter) return; - + if (!gaussianFilter) + return; + [_gaussianFilters removeAllObjects]; - for(NSNumber* radius in blurRadii) { - NSObject* newGaussianFilter = [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to make a new copy for every gaussianFilter? + for (NSNumber* radius in blurRadii) { + NSObject* newGaussianFilter = + [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to + // make a new copy for every gaussianFilter? [newGaussianFilter setValue:radius forKey:@"inputRadius"]; [_gaussianFilters addObject:newGaussianFilter]; } - } - else { - for(int i = 0; i < (int)[blurRadii count]; i++) { - if([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) continue; - + } else { + for (int i = 0; i < (int)[blurRadii count]; i++) { + if ([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) + continue; + [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } } - + self.layer.filters = _gaussianFilters; } From c501a80f838745ae066314da502ad16806f93b83 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Mon, 25 Jul 2022 13:44:41 -0700 Subject: [PATCH 16/51] Condensed applyBackdropFilter method --- .../framework/Source/FlutterPlatformViews.mm | 36 +++++------ .../Source/FlutterPlatformViewsTest.mm | 2 +- .../Source/FlutterPlatformViews_Internal.mm | 60 +++++++------------ 3 files changed, 41 insertions(+), 57 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 02c9c30c481a2..63299bfb0c3ba 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -404,9 +404,10 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CGRectGetHeight(flutter_view.bounds))] autorelease]; NSMutableArray* blurRadii = - [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: is autorelease the best option? + [[NSMutableArray alloc] init]; // TODO EMILY: is autorelease the best option? + NSNumber* blurRadius; - // int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -414,17 +415,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - - // if(numFilters < 1) { - // // TODO EMILY: these lines are for visual tests, delete before landing PR - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } + + // TODO EMILY: these lines are for visual simulator tests, delete before landing PR +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// } break; } @@ -442,13 +443,11 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. - if (!(*iter)->GetFilter().asBlur()) - continue; + if (!(*iter)->GetFilter().asBlur()) continue; // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D - // rendering. DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it - // is not used to blur the PlatformView. - NSNumber* blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); + // rendering. DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it is not used to blur the PlatformView. + blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); [blurRadii addObject:blurRadius]; break; } @@ -457,6 +456,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } [clipView applyBackdropFilters:blurRadii]; // TODO EMILY: pass pointer/reference? + [blurRadii release]; // Reverse the offset of the clipView. // The clipView's frame includes the final translate of the final transform matrix. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index f4cd8302e1da8..6131a048d99af 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -445,7 +445,7 @@ - (void)testRemoveBackdropFilters { flutter::MutatorsStack stack2; // Layer tree always pushes a screen scale factor to the stack stack2.PushTransform(screenScaleMatrix); - // Push backdrop filters + // Push backdrop filters //TODO EMILY: test pushing 0 backdrop filters, is that possible? for (int i = 0; i < 4; i++) { stack2.PushBackdropFilter(filter); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index bb5ed1d2bce62..c0bf4f20c3dee 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -81,15 +81,13 @@ - (NSObject*)extractGaussianFilter { UIView* view = [visualEffectView.subviews firstObject]; if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access its subviews."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; return nil; } NSObject* gaussianFilter = [[view.layer.filters firstObject] retain]; if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter. "; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter. "; return nil; } @@ -98,8 +96,7 @@ - (NSObject*)extractGaussianFilter { if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that // inputRadius key is valid? -> DOCUMENT ATTEMPTS - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter's properties."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter's properties."; return nil; } @@ -107,42 +104,29 @@ - (NSObject*)extractGaussianFilter { } - (void)applyBackdropFilters:(NSArray*)blurRadii { - if (!_gaussianFilters) { - NSObject* gaussianFilter = [self extractGaussianFilter]; - if (!gaussianFilter) - return; - - _gaussianFilters = - [[[NSMutableArray alloc] init] retain]; // TODO EMILY: does this need retain? - for (NSNumber* radius in blurRadii) { - NSObject* newGaussianFilter = - [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to - // make a new copy for every gaussianFilter? - [newGaussianFilter setValue:radius forKey:@"inputRadius"]; - [_gaussianFilters addObject:newGaussianFilter]; - } - } else if ([blurRadii count] != [_gaussianFilters count]) { - NSObject* gaussianFilter = [self extractGaussianFilter]; - if (!gaussianFilter) - return; + NSObject* gaussianFilter; + if(!_gaussianFilters) { + _gaussianFilters = [[[NSMutableArray alloc] init] retain]; + } - [_gaussianFilters removeAllObjects]; - for (NSNumber* radius in blurRadii) { - NSObject* newGaussianFilter = - [gaussianFilter copy]; // TODO EMILY: play with pointers and references. Do we need to - // make a new copy for every gaussianFilter? - [newGaussianFilter setValue:radius forKey:@"inputRadius"]; - [_gaussianFilters addObject:newGaussianFilter]; + if([blurRadii count] > [_gaussianFilters count]) { + gaussianFilter = [self extractGaussianFilter]; + if (!gaussianFilter) return; + } + + if ([blurRadii count] < [_gaussianFilters count]) { + [_gaussianFilters removeObjectsInRange:(NSRange){[blurRadii count], [_gaussianFilters count] - [blurRadii count]} ]; + } + + for (int i = 0; i < (int)[blurRadii count]; i++) { + if (i >= (int)[_gaussianFilters count]) { + [_gaussianFilters addObject:[gaussianFilter copy]]; } - } else { - for (int i = 0; i < (int)[blurRadii count]; i++) { - if ([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) - continue; + else if ([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) continue; - [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; - } + [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } - + self.layer.filters = _gaussianFilters; } From b4c64b6dfb5c621505228cf27ae007eb9002b18f Mon Sep 17 00:00:00 2001 From: emilyabest Date: Mon, 25 Jul 2022 13:45:43 -0700 Subject: [PATCH 17/51] Updated spacing. --- .../ios/framework/Source/FlutterPlatformViews_Internal.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index c0bf4f20c3dee..9d73feca573a8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -96,7 +96,7 @@ - (NSObject*)extractGaussianFilter { if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that // inputRadius key is valid? -> DOCUMENT ATTEMPTS - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter's properties."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation access the Gaussian blur filter's properties."; return nil; } From 0f147e9006eab31a3a10ec17eb75b7777543c0b6 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Mon, 25 Jul 2022 15:05:04 -0700 Subject: [PATCH 18/51] Formatted code --- .../framework/Source/FlutterPlatformViews.mm | 28 +++++++++--------- .../Source/FlutterPlatformViews_Internal.mm | 29 +++++++++++-------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 63299bfb0c3ba..b1d08d17eab0b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -407,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { [[NSMutableArray alloc] init]; // TODO EMILY: is autorelease the best option? NSNumber* blurRadius; -// int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -415,17 +415,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - + // TODO EMILY: these lines are for visual simulator tests, delete before landing PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// } + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // } break; } @@ -443,10 +443,12 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. - if (!(*iter)->GetFilter().asBlur()) continue; + if (!(*iter)->GetFilter().asBlur()) + continue; // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D - // rendering. DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it is not used to blur the PlatformView. + // rendering. DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it + // is not used to blur the PlatformView. blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); [blurRadii addObject:blurRadius]; break; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 9d73feca573a8..48a8d4c77cbab 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -81,13 +81,15 @@ - (NSObject*)extractGaussianFilter { UIView* view = [visualEffectView.subviews firstObject]; if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access its subviews."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access its subviews."; return nil; } NSObject* gaussianFilter = [[view.layer.filters firstObject] retain]; if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to access the Gaussian blur filter. "; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the Gaussian blur filter. "; return nil; } @@ -96,7 +98,8 @@ - (NSObject*)extractGaussianFilter { if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that // inputRadius key is valid? -> DOCUMENT ATTEMPTS - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation access the Gaussian blur filter's properties."; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " + "access the Gaussian blur filter's properties."; return nil; } @@ -105,28 +108,30 @@ - (NSObject*)extractGaussianFilter { - (void)applyBackdropFilters:(NSArray*)blurRadii { NSObject* gaussianFilter; - if(!_gaussianFilters) { + if (!_gaussianFilters) { _gaussianFilters = [[[NSMutableArray alloc] init] retain]; } - if([blurRadii count] > [_gaussianFilters count]) { + if ([blurRadii count] > [_gaussianFilters count]) { gaussianFilter = [self extractGaussianFilter]; - if (!gaussianFilter) return; + if (!gaussianFilter) + return; } - + if ([blurRadii count] < [_gaussianFilters count]) { - [_gaussianFilters removeObjectsInRange:(NSRange){[blurRadii count], [_gaussianFilters count] - [blurRadii count]} ]; + [_gaussianFilters removeObjectsInRange:(NSRange){[blurRadii count], + [_gaussianFilters count] - [blurRadii count]}]; } - + for (int i = 0; i < (int)[blurRadii count]; i++) { if (i >= (int)[_gaussianFilters count]) { [_gaussianFilters addObject:[gaussianFilter copy]]; - } - else if ([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) continue; + } else if ([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) + continue; [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } - + self.layer.filters = _gaussianFilters; } From 604113e680a9a5df9efd2fc8132ac5ae8f39a675 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 27 Jul 2022 10:43:32 -0700 Subject: [PATCH 19/51] Quick fix for failing applyBackdropFilters unit test. Started PR comment edits. --- flow/embedded_views.cc | 2 +- flow/embedded_views.h | 8 +-- .../framework/Source/FlutterPlatformViews.mm | 40 +++++------ .../Source/FlutterPlatformViewsTest.mm | 46 +++++++----- .../Source/FlutterPlatformViews_Internal.h | 2 + .../Source/FlutterPlatformViews_Internal.mm | 70 ++++++++++--------- 6 files changed, 94 insertions(+), 74 deletions(-) diff --git a/flow/embedded_views.cc b/flow/embedded_views.cc index 32acb94fc2296..9a6be65ac5c57 100644 --- a/flow/embedded_views.cc +++ b/flow/embedded_views.cc @@ -36,7 +36,7 @@ void MutatorsStack::PushOpacity(const int& alpha) { vector_.push_back(element); }; -void MutatorsStack::PushBackdropFilter(const DlImageFilter& filter) { +void MutatorsStack::PushBackdropFilter(DlImageFilter& filter) { std::shared_ptr element = std::make_shared(filter); vector_.push_back(element); }; diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 5e801dea40846..80ca96974d85f 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -70,8 +70,8 @@ class Mutator { explicit Mutator(const SkMatrix& matrix) : type_(kTransform), matrix_(matrix) {} explicit Mutator(const int& alpha) : type_(kOpacity), alpha_(alpha) {} - explicit Mutator(const DlImageFilter& filter) - : type_(kBackdropFilter), filter_(&filter) {} + explicit Mutator(DlImageFilter& filter) + : type_(kBackdropFilter), filter_(new DlBlurImageFilter(filter.asBlur())) {} // TODO EMILY: originally filter_(filter), hack for pushing different filters to the stack const MutatorType& GetType() const { return type_; } const SkRect& GetRect() const { return rect_; } @@ -125,7 +125,7 @@ class Mutator { SkMatrix matrix_; SkPath* path_; int alpha_; - const DlImageFilter* filter_; + DlImageFilter* filter_; //TODO EMILY const }; }; // Mutator @@ -148,7 +148,7 @@ class MutatorsStack { void PushClipPath(const SkPath& path); void PushTransform(const SkMatrix& matrix); void PushOpacity(const int& alpha); - void PushBackdropFilter(const DlImageFilter& filter); + void PushBackdropFilter(DlImageFilter& filter); // Removes the `Mutator` on the top of the stack // and destroys it. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b1d08d17eab0b..e34b598bc8da5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -384,7 +384,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { if (flutter_view_ == nullptr) { return; } - FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity)); ResetAnchor(embedded_view.layer); ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview; @@ -404,10 +403,11 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CGRectGetHeight(flutter_view.bounds))] autorelease]; NSMutableArray* blurRadii = - [[NSMutableArray alloc] init]; // TODO EMILY: is autorelease the best option? - NSNumber* blurRadius; - - // int numFilters = 0; + [[[NSMutableArray alloc] init] autorelease]; +// NSNumber* blurRadius; + + // TODO EMILY: this line is for visual simulator tests, delete before landing PR +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -417,15 +417,15 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { finalTransform = CATransform3DConcat(transform, finalTransform); // TODO EMILY: these lines are for visual simulator tests, delete before landing PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// } break; } @@ -443,22 +443,22 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. - if (!(*iter)->GetFilter().asBlur()) + if (!(*iter)->GetFilter().asBlur()) { continue; + } // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D - // rendering. DlBlurImageFilter's Tile Mode is not supported in CIGaussianBlurFilter so it + // rendering. DlBlurImageFilter's Tile Mode is not supported in Quartz's gaussianBlur CAFilter, so it // is not used to blur the PlatformView. - blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); - [blurRadii addObject:blurRadius]; +// blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); + [blurRadii addObject:@((*iter)->GetFilter().asBlur()->sigma_x())]; break; } } ++iter; } - [clipView applyBackdropFilters:blurRadii]; // TODO EMILY: pass pointer/reference? - [blurRadii release]; + [clipView applyBackdropFilters:blurRadii]; // Reverse the offset of the clipView. // The clipView's frame includes the final translate of the final transform matrix. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 6131a048d99af..edc10340785cf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -284,7 +284,7 @@ - (void)testApplyBackdropFilter { // Push a backdrop filter flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter( 5, 2, - flutter::DlTileMode::kClamp); // TODO EMILY: Should this be tested in a separate unit test? + flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); auto embeddedViewParams = @@ -292,7 +292,7 @@ - (void)testApplyBackdropFilter { flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); - XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:ChildClippingView.class]); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; [mockFlutterView addSubview:childClippingView]; @@ -350,8 +350,8 @@ - (void)testApplyMultipleBackdropFilters { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push backdrop filters - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); for (int i = 0; i < 50; i++) { + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(i, 2, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); } @@ -374,16 +374,13 @@ - (void)testApplyMultipleBackdropFilters { for (int i = 0; i < 50; i++) { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); - XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + XCTAssertEqualObjects(@(i), [gaussianFilter valueForKey:@"inputRadius"]); } // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } -// TODO: If a Flutter user removes the backdrop filter, a new stack is created. To simulate this -// removal in the test, I created a new stack. It doesn't test the removal's effect fully. I think -// we need to run further integration tests for full testing. - (void)testRemoveBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); @@ -445,7 +442,7 @@ - (void)testRemoveBackdropFilters { flutter::MutatorsStack stack2; // Layer tree always pushes a screen scale factor to the stack stack2.PushTransform(screenScaleMatrix); - // Push backdrop filters //TODO EMILY: test pushing 0 backdrop filters, is that possible? + // Push backdrop filters for (int i = 0; i < 4; i++) { stack2.PushBackdropFilter(filter); } @@ -470,11 +467,27 @@ - (void)testRemoveBackdropFilters { // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + + // Simulate removing all backdrop filters (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack3; + // Layer tree always pushes a screen scale factor to the stack + stack3.PushTransform(screenScaleMatrix); + // No backdrop filters are in the stack, so no backdrop filters need to be pushed + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack3); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has no CAFilters because no backdrop filters were added + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); } -// TODO: Similarly, to simulate a Flutter user editing the input radius of an applied Backdrop -// Filter, this test creates a new mutators stack. We might need more integration testing to fully -// test the functionality of editing a Backdrop Filter. - (void)testEditBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); @@ -537,11 +550,10 @@ - (void)testEditBackdropFilters { // Layer tree always pushes a screen scale factor to the stack stack2.PushTransform(screenScaleMatrix); // Push backdrop filters - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { if (i == 3) { flutter::DlBlurImageFilter filter2 = flutter::DlBlurImageFilter( - 2, 5, flutter::DlTileMode::kClamp); // TODO EMILY: is filter actually a reference? Should - // we be concerned about that? + 2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; } @@ -558,13 +570,13 @@ - (void)testEditBackdropFilters { [mockFlutterView layoutIfNeeded]; // childClippingView has CAFilters for the multiple backdrop filters - XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); // One filter has radius 2, others have original sigma x radius int countRadius2 = 0; int countRadius5 = 0; - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); if ([@(5) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) @@ -574,7 +586,7 @@ - (void)testEditBackdropFilters { } XCTAssertEqual(1, countRadius2); - XCTAssertEqual(3, countRadius5); + XCTAssertEqual(4, countRadius5); // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index e9724369f2516..4b6f6e5b9b3b9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -49,6 +49,8 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView +//@property + // Adds a blur filter to its layers. - (void)applyBackdropFilters:(NSArray*)blurRadii; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 48a8d4c77cbab..755484b7d0f55 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -57,7 +57,11 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter @implementation ChildClippingView { - NSMutableArray* _gaussianFilters; + // The gaussianFilters currently applied to this ChildClippingView. + NSMutableArray* _activeGaussianFilters; // property, nonatomic retain + + // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. + NSObject* _gaussianFilter; } // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to @@ -79,14 +83,7 @@ - (NSObject*)extractGaussianFilter { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - UIView* view = [visualEffectView.subviews firstObject]; - if (!view || ![view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access its subviews."; - return nil; - } - - NSObject* gaussianFilter = [[view.layer.filters firstObject] retain]; + NSObject* gaussianFilter = [[[visualEffectView.subviews firstObject].layer.filters firstObject] retain]; if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter. "; @@ -96,8 +93,7 @@ - (NSObject*)extractGaussianFilter { [visualEffectView release]; if (![[gaussianFilter valueForKey:@"inputRadius"] - isKindOfClass:[NSNumber class]]) { // TODO EMILY: is there another way to check that - // inputRadius key is valid? -> DOCUMENT ATTEMPTS + isKindOfClass:[NSNumber class]]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " "access the Gaussian blur filter's properties."; return nil; @@ -107,37 +103,47 @@ - (NSObject*)extractGaussianFilter { } - (void)applyBackdropFilters:(NSArray*)blurRadii { - NSObject* gaussianFilter; - if (!_gaussianFilters) { - _gaussianFilters = [[[NSMutableArray alloc] init] retain]; - } - - if ([blurRadii count] > [_gaussianFilters count]) { - gaussianFilter = [self extractGaussianFilter]; - if (!gaussianFilter) + if (!_activeGaussianFilters) { + _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; + + _gaussianFilter = [self extractGaussianFilter]; + if (!_gaussianFilter) { return; + } } - if ([blurRadii count] < [_gaussianFilters count]) { - [_gaussianFilters removeObjectsInRange:(NSRange){[blurRadii count], - [_gaussianFilters count] - [blurRadii count]}]; - } + bool updatedFilters = false; - for (int i = 0; i < (int)[blurRadii count]; i++) { - if (i >= (int)[_gaussianFilters count]) { - [_gaussianFilters addObject:[gaussianFilter copy]]; - } else if ([_gaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) + // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. + while ([blurRadii count] > [_activeGaussianFilters count]) { + // copy returns a deep copy of _gaussianFilter + [_activeGaussianFilters addObject:[_gaussianFilter copy]]; + updatedFilters = true; + } + while ([blurRadii count] < [_activeGaussianFilters count]) { + [_activeGaussianFilters removeLastObject]; + updatedFilters = true; + } + + for (NSUInteger i = 0; i < [blurRadii count]; i++) { + if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) { continue; - - [_gaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; + } + [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; + updatedFilters = true; } - self.layer.filters = _gaussianFilters; + if(updatedFilters) { + self.layer.filters = _activeGaussianFilters; + } } - (void)dealloc { - [_gaussianFilters release]; - _gaussianFilters = nil; + [_activeGaussianFilters release]; + _activeGaussianFilters = nil; + + [_gaussianFilter release]; + _gaussianFilter = nil; [super dealloc]; } From ade2908f62bc5d4ca6087ec1575b0202aa153a23 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 27 Jul 2022 15:15:16 -0700 Subject: [PATCH 20/51] Added unit tests for non-DlImageBlurFilters. Made activeGaussianFilters a property. --- flow/embedded_views.h | 14 +- .../framework/Source/FlutterPlatformViews.mm | 38 +- .../Source/FlutterPlatformViewsTest.mm | 361 ++++++++++++++++-- .../Source/FlutterPlatformViews_Internal.h | 3 +- .../Source/FlutterPlatformViews_Internal.mm | 18 +- 5 files changed, 368 insertions(+), 66 deletions(-) diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 80ca96974d85f..d0412ba23a1c1 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -56,7 +56,7 @@ class Mutator { alpha_ = other.alpha_; break; case kBackdropFilter: - filter_ = other.filter_; + filter_ = other.filter_->shared(); break; default: break; @@ -71,7 +71,11 @@ class Mutator { : type_(kTransform), matrix_(matrix) {} explicit Mutator(const int& alpha) : type_(kOpacity), alpha_(alpha) {} explicit Mutator(DlImageFilter& filter) - : type_(kBackdropFilter), filter_(new DlBlurImageFilter(filter.asBlur())) {} // TODO EMILY: originally filter_(filter), hack for pushing different filters to the stack + : type_(kBackdropFilter), + filter_(filter.shared()) { + } // TODO EMILY: originally filter_(filter), hack for pushing different + // filters to the stack + // Chris' idea: filter.shared().get() const MutatorType& GetType() const { return type_; } const SkRect& GetRect() const { return rect_; } @@ -114,10 +118,14 @@ class Mutator { if (type_ == kClipPath) { delete path_; } + // if(type_ == kBackdropFilter) { + // delete filter_; + // } }; private: MutatorType type_; + std::shared_ptr filter_; union { SkRect rect_; @@ -125,7 +133,7 @@ class Mutator { SkMatrix matrix_; SkPath* path_; int alpha_; - DlImageFilter* filter_; //TODO EMILY const + // DlImageFilter* filter_; //TODO EMILY const }; }; // Mutator diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index e34b598bc8da5..95085150c950b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -402,12 +402,10 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CGRectGetWidth(flutter_view.bounds), CGRectGetHeight(flutter_view.bounds))] autorelease]; - NSMutableArray* blurRadii = - [[[NSMutableArray alloc] init] autorelease]; -// NSNumber* blurRadius; - + NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; + // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -416,16 +414,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - // TODO EMILY: these lines are for visual simulator tests, delete before landing PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(5, 5, flutter::DlTileMode::kDecal); +// TODO EMILY: these lines are for visual simulator tests, delete before landing PR +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(5, 5, +// flutter::DlTileMode::kDecal); // -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; // -// numFilters++; -// } +// numFilters++; +// } break; } @@ -443,15 +442,12 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. - if (!(*iter)->GetFilter().asBlur()) { - continue; + if ((*iter)->GetFilter().asBlur()) { + // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D + // rendering. DlBlurImageFilter's Tile Mode is not supported in Quartz's gaussianBlur + // CAFilter, so it is not used to blur the PlatformView. + [blurRadii addObject:@((*iter)->GetFilter().asBlur()->sigma_x())]; } - - // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D - // rendering. DlBlurImageFilter's Tile Mode is not supported in Quartz's gaussianBlur CAFilter, so it - // is not used to blur the PlatformView. -// blurRadius = @((*iter)->GetFilter().asBlur()->sigma_x()); - [blurRadii addObject:@((*iter)->GetFilter().asBlur()->sigma_x())]; break; } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index edc10340785cf..bc8ba0248bdc5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -282,9 +282,7 @@ - (void)testApplyBackdropFilter { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push a backdrop filter - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter( - 5, 2, - flutter::DlTileMode::kClamp); + flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); auto embeddedViewParams = @@ -301,14 +299,13 @@ - (void)testApplyBackdropFilter { // childClippingView has the CAFilter, no additional filters were added XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); // sigmaX is chosen for input radius, regardless of sigmaY NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); - - // No new views were added - XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } - (void)testApplyMultipleBackdropFilters { @@ -351,7 +348,8 @@ - (void)testApplyMultipleBackdropFilters { stack.PushTransform(screenScaleMatrix); // Push backdrop filters for (int i = 0; i < 50; i++) { - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(i, 2, flutter::DlTileMode::kClamp); + flutter::DlBlurImageFilter filter = + flutter::DlBlurImageFilter(i, 2, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); } @@ -369,6 +367,8 @@ - (void)testApplyMultipleBackdropFilters { // childClippingView has CAFilters for the multiple backdrop filters XCTAssertEqual(50, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); // All filters have sigma X radius for (int i = 0; i < 50; i++) { @@ -376,9 +376,6 @@ - (void)testApplyMultipleBackdropFilters { XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); XCTAssertEqualObjects(@(i), [gaussianFilter valueForKey:@"inputRadius"]); } - - // No new views were added - XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } - (void)testRemoveBackdropFilters { @@ -467,25 +464,26 @@ - (void)testRemoveBackdropFilters { // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); - - - // Simulate removing all backdrop filters (create a new mutators stack) - // Create embedded view params - flutter::MutatorsStack stack3; - // Layer tree always pushes a screen scale factor to the stack - stack3.PushTransform(screenScaleMatrix); - // No backdrop filters are in the stack, so no backdrop filters need to be pushed + + // Simulate removing all backdrop filters (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // No backdrop filters in the stack, so no nothing to push embeddedViewParams = std::make_unique(screenScaleMatrix, - SkSize::Make(10, 10), stack3); + SkSize::Make(10, 10), stack2); flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); flutterPlatformViewsController->CompositeEmbeddedView(2); [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - + // childClippingView has no CAFilters because no backdrop filters were added XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } - (void)testEditBackdropFilters { @@ -544,7 +542,7 @@ - (void)testEditBackdropFilters { [mockFlutterView setNeedsLayout]; [mockFlutterView layoutIfNeeded]; - // Simulate editing 1 backdrop filter (create a new mutators stack) + // Simulate editing 1 backdrop filter in the middle of the stack (create a new mutators stack) // Create embedded view params flutter::MutatorsStack stack2; // Layer tree always pushes a screen scale factor to the stack @@ -552,8 +550,90 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 3) { - flutter::DlBlurImageFilter filter2 = flutter::DlBlurImageFilter( - 2, 5, flutter::DlTileMode::kClamp); + flutter::DlBlurImageFilter filter2 = + flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); + continue; + } + + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if (i == 3) { + XCTAssertEqualObjects(@(2), [gaussianFilter valueForKey:@"inputRadius"]); + } else { + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + } + + // Simulate editing 1 backdrop filter in the beginning of the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters + for (int i = 0; i < 5; i++) { + if (i == 0) { + flutter::DlBlurImageFilter filter2 = + flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); + continue; + } + + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if (i == 0) { + XCTAssertEqualObjects(@(2), [gaussianFilter valueForKey:@"inputRadius"]); + } else { + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + } + + // Simulate editing 1 backdrop filter in the end of the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters + for (int i = 0; i < 5; i++) { + if (i == 4) { + flutter::DlBlurImageFilter filter2 = + flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; } @@ -571,23 +651,240 @@ - (void)testEditBackdropFilters { // childClippingView has CAFilters for the multiple backdrop filters XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + if (i == 4) { + XCTAssertEqualObjects(@(2), [gaussianFilter valueForKey:@"inputRadius"]); + } else { + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + } + + // Simulate editing all backdrop filters in the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters + for (int i = 0; i < 5; i++) { + flutter::DlBlurImageFilter filter2 = + flutter::DlBlurImageFilter(i, 5, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(5, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The edited backdrop filter has the new radius value + for (int i = 0; i < 5; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + + XCTAssertEqualObjects(@(i), [gaussianFilter valueForKey:@"inputRadius"]); + } +} + +- (void)testApplyBackdropFilterNotDlBlurImageFilter { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a dilate backdrop filter + flutter::DlDilateImageFilter dilateFilter = flutter::DlDilateImageFilter(5, 2); + stack.PushBackdropFilter(dilateFilter); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // No filters were added + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // Simulate adding a non-DlBlurImageFilter in the middle of the stack (create a new mutators + // stack) Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters and dilate filter + flutter::DlBlurImageFilter blurFilter = + flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + for (int i = 0; i < 5; i++) { + if (i == 2) { + stack2.PushBackdropFilter(dilateFilter); + continue; + } + + stack2.PushBackdropFilter(blurFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Filters were only added for DlBlurImageFilters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The added filters are all gaussianBlur filters + for (int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // Simulate adding a non-DlBlurImageFilter to the beginning of the stack (replace the mutators + // stack) Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters and dilate filter + for (int i = 0; i < 5; i++) { + if (i == 0) { + stack2.PushBackdropFilter(dilateFilter); + continue; + } + + stack2.PushBackdropFilter(blurFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); - // One filter has radius 2, others have original sigma x radius - int countRadius2 = 0; - int countRadius5 = 0; + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Filters were only added for DlBlurImageFilters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + // The added filters are all gaussianBlur filters + for (int i = 0; i < 4; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // Simulate adding a non-DlBlurImageFilter to the end of the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push backdrop filters and dilate filter for (int i = 0; i < 5; i++) { + if (i == 4) { + stack2.PushBackdropFilter(dilateFilter); + continue; + } + + stack2.PushBackdropFilter(blurFilter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // Filters were only added for DlBlurImageFilters + XCTAssertEqual(4, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // The added filters are all gaussianBlur filters + for (int i = 0; i < 4; i++) { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); - if ([@(5) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) - countRadius5++; - else if ([@(2) isEqual:[gaussianFilter valueForKey:@"inputRadius"]]) - countRadius2++; + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } + + // Simulate adding only non-DlBlurImageFilter to the stack (replace the mutators stack) + // Update embedded view params, delete except screenScaleMatrix + for (int i = 0; i < 5; i++) { + stack2.Pop(); + } + // Push dilate filters + for (int i = 0; i < 5; i++) { + stack2.PushBackdropFilter(dilateFilter); } - XCTAssertEqual(1, countRadius2); - XCTAssertEqual(4, countRadius5); + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + // No filters were added + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); // No new views were added XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 4b6f6e5b9b3b9..06672513c94af 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -49,7 +49,8 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -//@property +// The gaussianFilters currently applied to this ChildClippingView. +@property(nonatomic, retain) NSMutableArray* activeGaussianFilters; // Adds a blur filter to its layers. - (void)applyBackdropFilters:(NSArray*)blurRadii; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 755484b7d0f55..d9abe4ad495e6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -57,9 +57,9 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter @implementation ChildClippingView { - // The gaussianFilters currently applied to this ChildClippingView. - NSMutableArray* _activeGaussianFilters; // property, nonatomic retain - +// // The gaussianFilters currently applied to this ChildClippingView. +// NSMutableArray* _activeGaussianFilters; // property, nonatomic retain + // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. NSObject* _gaussianFilter; } @@ -83,7 +83,8 @@ - (NSObject*)extractGaussianFilter { UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - NSObject* gaussianFilter = [[[visualEffectView.subviews firstObject].layer.filters firstObject] retain]; + NSObject* gaussianFilter = + [[[visualEffectView.subviews firstObject].layer.filters firstObject] retain]; if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter. "; @@ -92,8 +93,7 @@ - (NSObject*)extractGaussianFilter { [visualEffectView release]; - if (![[gaussianFilter valueForKey:@"inputRadius"] - isKindOfClass:[NSNumber class]]) { + if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " "access the Gaussian blur filter's properties."; return nil; @@ -105,7 +105,7 @@ - (NSObject*)extractGaussianFilter { - (void)applyBackdropFilters:(NSArray*)blurRadii { if (!_activeGaussianFilters) { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; - + _gaussianFilter = [self extractGaussianFilter]; if (!_gaussianFilter) { return; @@ -124,7 +124,7 @@ - (void)applyBackdropFilters:(NSArray*)blurRadii { [_activeGaussianFilters removeLastObject]; updatedFilters = true; } - + for (NSUInteger i = 0; i < [blurRadii count]; i++) { if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) { continue; @@ -133,7 +133,7 @@ - (void)applyBackdropFilters:(NSArray*)blurRadii { updatedFilters = true; } - if(updatedFilters) { + if (updatedFilters) { self.layer.filters = _activeGaussianFilters; } } From 6a1e014cc6221bb83a43cca1f44910f0d38e0b25 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 27 Jul 2022 15:21:33 -0700 Subject: [PATCH 21/51] Formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 24 +++++++++---------- .../Source/FlutterPlatformViews_Internal.mm | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 95085150c950b..ce624332de59c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -405,7 +405,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -414,17 +414,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); -// TODO EMILY: these lines are for visual simulator tests, delete before landing PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(5, 5, -// flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// } + // TODO EMILY: these lines are for visual simulator tests, delete before landing PR + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, + // flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // } break; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index d9abe4ad495e6..65da30847aecd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -57,8 +57,8 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter @implementation ChildClippingView { -// // The gaussianFilters currently applied to this ChildClippingView. -// NSMutableArray* _activeGaussianFilters; // property, nonatomic retain + // // The gaussianFilters currently applied to this ChildClippingView. + // NSMutableArray* _activeGaussianFilters; // property, nonatomic retain // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. NSObject* _gaussianFilter; From e07cb72df13315268a0cb124166ca89e541fb107 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 28 Jul 2022 09:25:34 -0700 Subject: [PATCH 22/51] Minor formatting changes --- .../framework/Source/FlutterPlatformViews.mm | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index ce624332de59c..1248951813982 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -405,7 +405,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; + int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -414,17 +414,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - // TODO EMILY: these lines are for visual simulator tests, delete before landing PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, - // flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } +// TODO EMILY: these lines are for visual simulator tests, delete before landing PR + if(numFilters < 1) { + flutter::DlBlurImageFilter filter = + flutter::DlBlurImageFilter(5, 5, + flutter::DlTileMode::kDecal); + + NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + [blurRadii addObject:blurRadius]; + + numFilters++; + } break; } From d9bcea9fcacd9a0dd8c66dd195bbe9d119c0e018 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 28 Jul 2022 16:10:58 -0700 Subject: [PATCH 23/51] Minor changes for visual testing. --- .../framework/Source/FlutterPlatformViews.mm | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 1248951813982..3106607acfa54 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -405,7 +405,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -414,18 +414,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); -// TODO EMILY: these lines are for visual simulator tests, delete before landing PR - if(numFilters < 1) { - flutter::DlBlurImageFilter filter = - flutter::DlBlurImageFilter(5, 5, - flutter::DlTileMode::kDecal); - - NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - [blurRadii addObject:blurRadius]; - - numFilters++; - } - +// TODO EMILY: these lines are for visual simulator tests, delete before landing PR +// if(numFilters < 2) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(1, 1, +// flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// } break; } case kClipRect: From 4ae563b0c5f9a3ce71cff95ff0c8c8838d566f43 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 28 Jul 2022 16:17:35 -0700 Subject: [PATCH 24/51] Ran formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 3106607acfa54..d0cea5514408d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -405,7 +405,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -414,17 +414,18 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); -// TODO EMILY: these lines are for visual simulator tests, delete before landing PR -// if(numFilters < 2) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(1, 1, -// flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// } + // TODO EMILY: these lines are for visual simulator tests, delete before landing + // PR + // if(numFilters < 2) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(1, 1, + // flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // } break; } case kClipRect: From b18c2adbf0d19204659a9e762fe68b46e993d636 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Mon, 1 Aug 2022 10:59:26 -0700 Subject: [PATCH 25/51] Updated code to work with Chris' PR --- flow/embedded_views.h | 3 +- .../framework/Source/FlutterPlatformViews.mm | 22 +++++++------- .../Source/FlutterPlatformViewsTest.mm | 30 +++++++++---------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/flow/embedded_views.h b/flow/embedded_views.h index df17d4bf18270..64c6a4be7ab23 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -56,7 +56,7 @@ class Mutator { alpha_ = other.alpha_; break; case kBackdropFilter: - filter_ = other.filter_->shared(); + filter_ = other.filter_; break; default: break; @@ -121,7 +121,6 @@ class Mutator { private: MutatorType type_; - std::shared_ptr filter_; // TODO(cyanglaz): Remove union. // https://github.com/flutter/flutter/issues/108470 diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index a1b1b79e7ba61..d39e7e9d7227f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -404,7 +404,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; + int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -415,16 +415,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 2) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(1, 1, - // flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } + if(numFilters < 2) { + flutter::DlBlurImageFilter filter = + flutter::DlBlurImageFilter(1, 1, + flutter::DlTileMode::kDecal); + + NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + [blurRadii addObject:blurRadius]; + + numFilters++; + } break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index bc8ba0248bdc5..7d83b3e6d4cfe 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -282,7 +282,7 @@ - (void)testApplyBackdropFilter { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push a backdrop filter - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); auto embeddedViewParams = @@ -348,8 +348,7 @@ - (void)testApplyMultipleBackdropFilters { stack.PushTransform(screenScaleMatrix); // Push backdrop filters for (int i = 0; i < 50; i++) { - flutter::DlBlurImageFilter filter = - flutter::DlBlurImageFilter(i, 2, flutter::DlTileMode::kClamp); + auto filter = std::make_shared(i, 2, flutter::DlTileMode::kClamp); stack.PushBackdropFilter(filter); } @@ -417,7 +416,7 @@ - (void)testRemoveBackdropFilters { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push backdrop filters - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); for (int i = 0; i < 5; i++) { stack.PushBackdropFilter(filter); } @@ -525,7 +524,7 @@ - (void)testEditBackdropFilters { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push backdrop filters - flutter::DlBlurImageFilter filter = flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); for (int i = 0; i < 5; i++) { stack.PushBackdropFilter(filter); } @@ -550,8 +549,8 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 3) { - flutter::DlBlurImageFilter filter2 = - flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); + auto filter2 = std::make_shared(2, 5, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); continue; } @@ -591,8 +590,7 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 0) { - flutter::DlBlurImageFilter filter2 = - flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); + auto filter2 = std::make_shared(2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; } @@ -632,8 +630,7 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 4) { - flutter::DlBlurImageFilter filter2 = - flutter::DlBlurImageFilter(2, 5, flutter::DlTileMode::kClamp); + auto filter2 = std::make_shared(2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; } @@ -672,8 +669,8 @@ - (void)testEditBackdropFilters { } // Push backdrop filters for (int i = 0; i < 5; i++) { - flutter::DlBlurImageFilter filter2 = - flutter::DlBlurImageFilter(i, 5, flutter::DlTileMode::kClamp); + auto filter2 = std::make_shared(i, 2, flutter::DlTileMode::kClamp); + stack2.PushBackdropFilter(filter2); } @@ -738,7 +735,7 @@ - (void)testApplyBackdropFilterNotDlBlurImageFilter { SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); stack.PushTransform(screenScaleMatrix); // Push a dilate backdrop filter - flutter::DlDilateImageFilter dilateFilter = flutter::DlDilateImageFilter(5, 2); + auto dilateFilter = std::make_shared(5, 2); stack.PushBackdropFilter(dilateFilter); auto embeddedViewParams = @@ -764,8 +761,9 @@ - (void)testApplyBackdropFilterNotDlBlurImageFilter { // Layer tree always pushes a screen scale factor to the stack stack2.PushTransform(screenScaleMatrix); // Push backdrop filters and dilate filter - flutter::DlBlurImageFilter blurFilter = - flutter::DlBlurImageFilter(5, 2, flutter::DlTileMode::kClamp); + auto blurFilter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + + for (int i = 0; i < 5; i++) { if (i == 2) { stack2.PushBackdropFilter(dilateFilter); From 732c94d9feffc0d71126329dd450d52ed3ff9432 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Mon, 1 Aug 2022 13:06:37 -0700 Subject: [PATCH 26/51] Formatted code --- .../framework/Source/FlutterPlatformViews.mm | 22 +++++++++---------- .../Source/FlutterPlatformViewsTest.mm | 10 +++++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index d39e7e9d7227f..b2ddd8d385bc9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -404,7 +404,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -415,16 +415,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - if(numFilters < 2) { - flutter::DlBlurImageFilter filter = - flutter::DlBlurImageFilter(1, 1, - flutter::DlTileMode::kDecal); - - NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - [blurRadii addObject:blurRadius]; - - numFilters++; - } + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, + // flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // } break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 7d83b3e6d4cfe..f6fea68750ff4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -549,7 +549,8 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 3) { - auto filter2 = std::make_shared(2, 5, flutter::DlTileMode::kClamp); + auto filter2 = + std::make_shared(2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; @@ -590,7 +591,8 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 0) { - auto filter2 = std::make_shared(2, 5, flutter::DlTileMode::kClamp); + auto filter2 = + std::make_shared(2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; } @@ -630,7 +632,8 @@ - (void)testEditBackdropFilters { // Push backdrop filters for (int i = 0; i < 5; i++) { if (i == 4) { - auto filter2 = std::make_shared(2, 5, flutter::DlTileMode::kClamp); + auto filter2 = + std::make_shared(2, 5, flutter::DlTileMode::kClamp); stack2.PushBackdropFilter(filter2); continue; } @@ -763,7 +766,6 @@ - (void)testApplyBackdropFilterNotDlBlurImageFilter { // Push backdrop filters and dilate filter auto blurFilter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); - for (int i = 0; i < 5; i++) { if (i == 2) { stack2.PushBackdropFilter(dilateFilter); From d6066ec7602f937c50f86a48d2619c69af67bf55 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 2 Aug 2022 09:30:32 -0700 Subject: [PATCH 27/51] Resolved memory leak failure. Commented pass by reference alternative. --- flow/embedded_views.h | 3 - .../framework/Source/FlutterPlatformViews.mm | 36 +++++----- .../Source/FlutterPlatformViews_Internal.h | 9 +-- .../Source/FlutterPlatformViews_Internal.mm | 65 ++++++++++++++++--- 4 files changed, 79 insertions(+), 34 deletions(-) diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 64c6a4be7ab23..394fa9d829a92 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -114,9 +114,6 @@ class Mutator { if (type_ == kClipPath) { delete path_; } - // if(type_ == kBackdropFilter) { - // delete filter_; - // } }; private: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b2ddd8d385bc9..512e1142b7ee8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -19,6 +19,7 @@ #import "flutter/shell/platform/darwin/ios/ios_surface.h" @implementation UIView (FirstResponder) + - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { if (self.isFirstResponder) { return YES; @@ -33,6 +34,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { @end namespace flutter { +// Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied. +BOOL canApplyBlurBackdrop = YES; std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( GrDirectContext* gr_context, @@ -198,7 +201,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease]; [clipping_view addSubview:touch_interceptor]; root_views_[viewId] = fml::scoped_nsobject([clipping_view retain]); - + result(nil); } @@ -404,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -415,16 +418,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, - // flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(5, 5, +// flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// } break; } case kClipRect: @@ -441,7 +444,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. - if ((*iter)->GetFilter().asBlur()) { + if ((*iter)->GetFilter().asBlur() && canApplyBlurBackdrop) { // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D // rendering. DlBlurImageFilter's Tile Mode is not supported in Quartz's gaussianBlur // CAFilter, so it is not used to blur the PlatformView. @@ -452,8 +455,11 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } ++iter; } - - [clipView applyBackdropFilters:blurRadii]; + + if(canApplyBlurBackdrop) { + canApplyBlurBackdrop = [clipView applyBlurBackdropFilters:blurRadii]; + } + // Reverse the offset of the clipView. // The clipView's frame includes the final translate of the final transform matrix. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 06672513c94af..b7ab0420f150b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -5,7 +5,6 @@ #ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_ -#include "flutter/display_list/display_list_image_filter.h" #include "flutter/flow/embedded_views.h" #include "flutter/flow/rtree.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" @@ -49,11 +48,9 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -// The gaussianFilters currently applied to this ChildClippingView. -@property(nonatomic, retain) NSMutableArray* activeGaussianFilters; - -// Adds a blur filter to its layers. -- (void)applyBackdropFilters:(NSArray*)blurRadii; +// Adds a blur filter to its layers. Returns false if Apple's API has changed and +// the blur backdrop filters cannot be applied, otherwise returns true. +- (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 65da30847aecd..0bf40d62c8571 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#include "flutter/display_list/display_list_image_filter.h" #include "flutter/fml/platform/darwin/cf_utils.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" @@ -56,10 +57,12 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@implementation ChildClippingView { - // // The gaussianFilters currently applied to this ChildClippingView. - // NSMutableArray* _activeGaussianFilters; // property, nonatomic retain +@interface ChildClippingView() +// The gaussianFilters currently applied to this ChildClippingView. +@property(nonatomic, retain) NSMutableArray* activeGaussianFilters; +@end +@implementation ChildClippingView { // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. NSObject* _gaussianFilter; } @@ -79,13 +82,20 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { // Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its // gaussianFilter. Logs errors and returns if Apple's API has changed and the filter can't be // extracted. -- (NSObject*)extractGaussianFilter { ++ (NSObject*)extractGaussianFilter { // TODO would pass by reference make more sense? UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - NSObject* gaussianFilter = - [[[visualEffectView.subviews firstObject].layer.filters firstObject] retain]; - if (!gaussianFilter || ![[gaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + NSObject* gaussianFilter; //TODO EMILY: Weiyu's for loop works, but only if retain is included, why? + for(CIFilter* filter in [visualEffectView.subviews firstObject].layer.filters) { + if([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + gaussianFilter = filter; + [gaussianFilter retain]; + break; + } + } + + if (!gaussianFilter) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the Gaussian blur filter. "; return nil; @@ -96,19 +106,53 @@ - (NSObject*)extractGaussianFilter { if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " "access the Gaussian blur filter's properties."; + [gaussianFilter release]; return nil; } +// [gaussianFilter release]; return gaussianFilter; } -- (void)applyBackdropFilters:(NSArray*)blurRadii { +// TODO EMILY: pass by reference works, but only if retain is still included +//- (void)extractGaussianFilter:(NSObject**)gaussianFilter { +// UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] +// initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; +// +//// NSObject* gaussianFilter; +//// for(CIFilter* filter in [visualEffectView.subviews firstObject].layer.filters) { +//// if([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { +//// gaussianFilter = filter; +////// [gaussianFilter retain]; +//// break; +//// } +//// } +// +// NSObject* extractedGaussianFilter = +// [[visualEffectView.subviews firstObject].layer.filters firstObject]; // TODO why do we retain? -> make a note +// if (!extractedGaussianFilter || ![[extractedGaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { +// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " +// "access the Gaussian blur filter. "; +// } +// +// [visualEffectView release]; +// +// if (![[extractedGaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { +// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " +// "access the Gaussian blur filter's properties."; +// } +// +// *gaussianFilter = extractedGaussianFilter; +//} + +- (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { if (!_activeGaussianFilters) { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; - _gaussianFilter = [self extractGaussianFilter]; +// [self extractGaussianFilter:&_gaussianFilter]; + _gaussianFilter = [ChildClippingView extractGaussianFilter]; if (!_gaussianFilter) { - return; + return false; } } @@ -136,6 +180,7 @@ - (void)applyBackdropFilters:(NSArray*)blurRadii { if (updatedFilters) { self.layer.filters = _activeGaussianFilters; } + return true; } - (void)dealloc { From 2450016749ff74af71a05438261f9d36bf530c19 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 2 Aug 2022 15:36:58 -0700 Subject: [PATCH 28/51] Reorganized extraction method to use a for loop --- .../framework/Source/FlutterPlatformViews.mm | 7 +- .../Source/FlutterPlatformViews_Internal.h | 4 +- .../Source/FlutterPlatformViews_Internal.mm | 99 +++++++------------ 3 files changed, 39 insertions(+), 71 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 512e1142b7ee8..3818111018650 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -445,9 +445,10 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { case kBackdropFilter: { // We only support DlBlurImageFilter for BackdropFilter. if ((*iter)->GetFilter().asBlur() && canApplyBlurBackdrop) { - // Sigma X is arbitrarily chosen as the radius value because Quartz only supports 1D - // rendering. DlBlurImageFilter's Tile Mode is not supported in Quartz's gaussianBlur - // CAFilter, so it is not used to blur the PlatformView. + // sigma_x is arbitrarily chosen as the radius value because Quartz sets + // sigma_x and sigma_y equal to each other. DlBlurImageFilter's Tile Mode + // is not supported in Quartz's gaussianBlur CAFilter, so it is not used + // to blur the PlatformView. [blurRadii addObject:@((*iter)->GetFilter().asBlur()->sigma_x())]; } break; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index b7ab0420f150b..b18d446989ad2 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -48,8 +48,8 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -// Adds a blur filter to its layers. Returns false if Apple's API has changed and -// the blur backdrop filters cannot be applied, otherwise returns true. +// Adds a blur filter to its layers. Returns NO if Apple's API has changed and +// blurred backdrop filters cannot be applied, otherwise returns YES. - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 0bf40d62c8571..94ff6bbf0cb06 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -80,93 +80,60 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { } // Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its -// gaussianFilter. Logs errors and returns if Apple's API has changed and the filter can't be -// extracted. -+ (NSObject*)extractGaussianFilter { // TODO would pass by reference make more sense? - UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - - NSObject* gaussianFilter; //TODO EMILY: Weiyu's for loop works, but only if retain is included, why? - for(CIFilter* filter in [visualEffectView.subviews firstObject].layer.filters) { - if([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { - gaussianFilter = filter; - [gaussianFilter retain]; +// gaussianFilter. Returns nil if Apple's API has changed and the filter cannot be extracted. ++ (NSObject*)extractGaussianFilter { + UIVisualEffectView* visualEffectView = [[[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] autorelease]; + + NSObject* gaussianFilter = nil; + + for(UIView* view in visualEffectView.subviews) { + if([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for(CIFilter* filter in view.layer.filters) { + if([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + if ([[filter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { + gaussianFilter = filter; + } + // No need to look at other CIFilters. If the API structure has not changed, the gaussianBlur + // filter was succesfully saved. Otherwise, still exit the loop because the filter cannot + // be extracted. + break; + } + } + // No need to look at other UIViews. If the API structure has not changed, the gaussianBlur + // filter was succesfully saved. Otherwise, still exit the loop because the filter cannot + // be extracted. break; } } - if (!gaussianFilter) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the Gaussian blur filter. "; - return nil; - } - - [visualEffectView release]; - - if (![[gaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { - FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " - "access the Gaussian blur filter's properties."; - [gaussianFilter release]; - return nil; - } - -// [gaussianFilter release]; return gaussianFilter; } -// TODO EMILY: pass by reference works, but only if retain is still included -//- (void)extractGaussianFilter:(NSObject**)gaussianFilter { -// UIVisualEffectView* visualEffectView = [[UIVisualEffectView alloc] -// initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; -// -//// NSObject* gaussianFilter; -//// for(CIFilter* filter in [visualEffectView.subviews firstObject].layer.filters) { -//// if([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { -//// gaussianFilter = filter; -////// [gaussianFilter retain]; -//// break; -//// } -//// } -// -// NSObject* extractedGaussianFilter = -// [[visualEffectView.subviews firstObject].layer.filters firstObject]; // TODO why do we retain? -> make a note -// if (!extractedGaussianFilter || ![[extractedGaussianFilter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { -// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " -// "access the Gaussian blur filter. "; -// } -// -// [visualEffectView release]; -// -// if (![[extractedGaussianFilter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { -// FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation " -// "access the Gaussian blur filter's properties."; -// } -// -// *gaussianFilter = extractedGaussianFilter; -//} - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { if (!_activeGaussianFilters) { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; -// [self extractGaussianFilter:&_gaussianFilter]; _gaussianFilter = [ChildClippingView extractGaussianFilter]; if (!_gaussianFilter) { - return false; + FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " + "access the gaussianBlur CAFilter."; + return NO; } } - bool updatedFilters = false; + BOOL updatedFilters = NO; // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. while ([blurRadii count] > [_activeGaussianFilters count]) { // copy returns a deep copy of _gaussianFilter [_activeGaussianFilters addObject:[_gaussianFilter copy]]; - updatedFilters = true; + updatedFilters = YES; } while ([blurRadii count] < [_activeGaussianFilters count]) { [_activeGaussianFilters removeLastObject]; - updatedFilters = true; + updatedFilters = YES; } for (NSUInteger i = 0; i < [blurRadii count]; i++) { @@ -174,21 +141,21 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { continue; } [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; - updatedFilters = true; + updatedFilters = YES; } if (updatedFilters) { self.layer.filters = _activeGaussianFilters; } - return true; + return YES; } - (void)dealloc { [_activeGaussianFilters release]; _activeGaussianFilters = nil; - [_gaussianFilter release]; - _gaussianFilter = nil; +// [_gaussianFilter release]; +// _gaussianFilter = nil; [super dealloc]; } From 995fd5763e2f1e35143e9aa57b2637a83c85ef03 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 2 Aug 2022 15:42:53 -0700 Subject: [PATCH 29/51] Formatted code. Previous commit also updates API checks --- .../framework/Source/FlutterPlatformViews.mm | 29 +++++++++---------- .../Source/FlutterPlatformViews_Internal.mm | 27 +++++++++-------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 3818111018650..fa9f3a2035ad7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -201,7 +201,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { [[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease]; [clipping_view addSubview:touch_interceptor]; root_views_[viewId] = fml::scoped_nsobject([clipping_view retain]); - + result(nil); } @@ -407,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -418,16 +418,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(5, 5, -// flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// } + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, + // flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // } break; } case kClipRect: @@ -456,11 +456,10 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } ++iter; } - - if(canApplyBlurBackdrop) { + + if (canApplyBlurBackdrop) { canApplyBlurBackdrop = [clipView applyBlurBackdropFilters:blurRadii]; } - // Reverse the offset of the clipView. // The clipView's frame includes the final translate of the final transform matrix. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 94ff6bbf0cb06..cd559a55e6095 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -57,7 +57,7 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@interface ChildClippingView() +@interface ChildClippingView () // The gaussianFilters currently applied to this ChildClippingView. @property(nonatomic, retain) NSMutableArray* activeGaussianFilters; @end @@ -83,20 +83,20 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { // gaussianFilter. Returns nil if Apple's API has changed and the filter cannot be extracted. + (NSObject*)extractGaussianFilter { UIVisualEffectView* visualEffectView = [[[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] autorelease]; + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] autorelease]; NSObject* gaussianFilter = nil; - - for(UIView* view in visualEffectView.subviews) { - if([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - for(CIFilter* filter in view.layer.filters) { - if([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + + for (UIView* view in visualEffectView.subviews) { + if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for (CIFilter* filter in view.layer.filters) { + if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { if ([[filter valueForKey:@"inputRadius"] isKindOfClass:[NSNumber class]]) { gaussianFilter = filter; } - // No need to look at other CIFilters. If the API structure has not changed, the gaussianBlur - // filter was succesfully saved. Otherwise, still exit the loop because the filter cannot - // be extracted. + // No need to look at other CIFilters. If the API structure has not changed, the + // gaussianBlur filter was succesfully saved. Otherwise, still exit the loop because the + // filter cannot be extracted. break; } } @@ -110,7 +110,6 @@ + (NSObject*)extractGaussianFilter { return gaussianFilter; } - - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { if (!_activeGaussianFilters) { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; @@ -118,7 +117,7 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { _gaussianFilter = [ChildClippingView extractGaussianFilter]; if (!_gaussianFilter) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " - "access the gaussianBlur CAFilter."; + "access the gaussianBlur CAFilter."; return NO; } } @@ -154,8 +153,8 @@ - (void)dealloc { [_activeGaussianFilters release]; _activeGaussianFilters = nil; -// [_gaussianFilter release]; -// _gaussianFilter = nil; + [_gaussianFilter release]; + _gaussianFilter = nil; [super dealloc]; } From 20a7d8f00cad93d2b0dfb72d3020c985590af45a Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 3 Aug 2022 13:10:02 -0700 Subject: [PATCH 30/51] Added unit test for invalid UIVEV API --- .../framework/Source/FlutterPlatformViews.mm | 3 +- .../Source/FlutterPlatformViewsTest.mm | 62 +++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.h | 5 ++ .../Source/FlutterPlatformViews_Internal.mm | 21 +++++-- 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index fa9f3a2035ad7..cec6aecc74307 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -19,7 +19,6 @@ #import "flutter/shell/platform/darwin/ios/ios_surface.h" @implementation UIView (FirstResponder) - - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { if (self.isFirstResponder) { return YES; @@ -457,7 +456,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { ++iter; } - if (canApplyBlurBackdrop) { + if (canApplyBlurBackdrop && ([blurRadii count] > 0)) { canApplyBlurBackdrop = [clipView applyBlurBackdropFilters:blurRadii]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index f6fea68750ff4..a892968c8a3fe 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -889,6 +889,68 @@ - (void)testApplyBackdropFilterNotDlBlurImageFilter { XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); } +- (void)testApplyBackdropFilterAPIChanged { + NSArray* blurRadii = @[@(1), @(5), @(10)]; + + // The gaussianBlur filter is extracted once for each childClippingView. + // Each test requires a new childClippingView + // Valid UIVisualEffectView API + ChildClippingView* childClippingView1 = [[ChildClippingView alloc] init]; + childClippingView1.viewToExtractFrom = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + XCTAssertTrue([childClippingView1 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIVisualEffectView initialization + ChildClippingView* childClippingView2 = [[ChildClippingView alloc] init]; + childClippingView2.viewToExtractFrom = [[UIVisualEffectView alloc] init]; + XCTAssertFalse([childClippingView2 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIView + ChildClippingView* childClippingView3 = [[ChildClippingView alloc] init]; + childClippingView3.viewToExtractFrom = [[UIView alloc] init]; + XCTAssertFalse([childClippingView3 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIVisualEffectView API for "name" + UIVisualEffectView* editedUIVisualEffectView1 = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + NSArray* subviews1 = editedUIVisualEffectView1.subviews; + for(UIView* view in subviews1) { + if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for(CIFilter* filter in view.layer.filters) { + if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + [filter setValue:@"notGaussianBlur" forKey:@"name"]; + break; + } + } + break; + } + } + + ChildClippingView* childClippingView4 = [[ChildClippingView alloc] init]; + childClippingView4.viewToExtractFrom = editedUIVisualEffectView1; + XCTAssertFalse([childClippingView4 applyBlurBackdropFilters:blurRadii]); + + // Invalid UIVisualEffectView API for "inputRadius" + UIVisualEffectView* editedUIVisualEffectView2 = [[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + NSArray* subviews2 = editedUIVisualEffectView2.subviews; + for(UIView* view in subviews2) { + if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { + for(CIFilter* filter in view.layer.filters) { + if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { + [filter setValue:@"invalidInputRadius" forKey:@"inputRadius"]; + break; + } + } + break; + } + } + + ChildClippingView* childClippingView5 = [[ChildClippingView alloc] init]; + childClippingView5.viewToExtractFrom = editedUIVisualEffectView2; + XCTAssertFalse([childClippingView5 applyBlurBackdropFilters:blurRadii]); +} + - (void)testCompositePlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index b18d446989ad2..5cf990e8d566f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -52,6 +52,11 @@ // blurred backdrop filters cannot be applied, otherwise returns YES. - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii; +// The UIView used to extract the gaussianBlur filter. This must be a UIVisualEffectView +// initalized with UIBlurEffect to extract the correct filter. Made a public property +// for custom unit tests. +@property (nonatomic, strong) UIView* viewToExtractFrom; + @end namespace flutter { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index cd559a55e6095..6bd4bcf6067a4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -67,6 +67,17 @@ @implementation ChildClippingView { NSObject* _gaussianFilter; } +// Lazy initializes viewToExtractFrom as the expected UIVisualEffectView. The backdropFilter blur +// requires this UIVisualEffectView initialization. The lazy initalization is only used to allow +// custom unit tests. +- (UIView*) viewToExtractFrom { + if(!_viewToExtractFrom) { + _viewToExtractFrom = [[[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; + } + return _viewToExtractFrom; +} + // 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. @@ -81,13 +92,10 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { // Creates and initializes a UIVisualEffectView with a UIBlurEffect. Extracts and returns its // gaussianFilter. Returns nil if Apple's API has changed and the filter cannot be extracted. -+ (NSObject*)extractGaussianFilter { - UIVisualEffectView* visualEffectView = [[[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] autorelease]; - +- (NSObject*)extractGaussianFilter { NSObject* gaussianFilter = nil; - for (UIView* view in visualEffectView.subviews) { + for (UIView* view in self.viewToExtractFrom.subviews) { if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { for (CIFilter* filter in view.layer.filters) { if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { @@ -114,12 +122,13 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { if (!_activeGaussianFilters) { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; - _gaussianFilter = [ChildClippingView extractGaussianFilter]; + _gaussianFilter = [self extractGaussianFilter]; if (!_gaussianFilter) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the gaussianBlur CAFilter."; return NO; } + [self.viewToExtractFrom release]; } BOOL updatedFilters = NO; From d81676f117476de01e0a4f042945dcd8a1494ebb Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 3 Aug 2022 13:15:09 -0700 Subject: [PATCH 31/51] Formatting check --- .../Source/FlutterPlatformViewsTest.mm | 24 +++++++++---------- .../Source/FlutterPlatformViews_Internal.h | 2 +- .../Source/FlutterPlatformViews_Internal.mm | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index a892968c8a3fe..7694db9ea00f1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -890,33 +890,33 @@ - (void)testApplyBackdropFilterNotDlBlurImageFilter { } - (void)testApplyBackdropFilterAPIChanged { - NSArray* blurRadii = @[@(1), @(5), @(10)]; - + NSArray* blurRadii = @[ @(1), @(5), @(10) ]; + // The gaussianBlur filter is extracted once for each childClippingView. // Each test requires a new childClippingView // Valid UIVisualEffectView API ChildClippingView* childClippingView1 = [[ChildClippingView alloc] init]; childClippingView1.viewToExtractFrom = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; XCTAssertTrue([childClippingView1 applyBlurBackdropFilters:blurRadii]); - + // Invalid UIVisualEffectView initialization ChildClippingView* childClippingView2 = [[ChildClippingView alloc] init]; childClippingView2.viewToExtractFrom = [[UIVisualEffectView alloc] init]; XCTAssertFalse([childClippingView2 applyBlurBackdropFilters:blurRadii]); - + // Invalid UIView ChildClippingView* childClippingView3 = [[ChildClippingView alloc] init]; childClippingView3.viewToExtractFrom = [[UIView alloc] init]; XCTAssertFalse([childClippingView3 applyBlurBackdropFilters:blurRadii]); - + // Invalid UIVisualEffectView API for "name" UIVisualEffectView* editedUIVisualEffectView1 = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; NSArray* subviews1 = editedUIVisualEffectView1.subviews; - for(UIView* view in subviews1) { + for (UIView* view in subviews1) { if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - for(CIFilter* filter in view.layer.filters) { + for (CIFilter* filter in view.layer.filters) { if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { [filter setValue:@"notGaussianBlur" forKey:@"name"]; break; @@ -932,11 +932,11 @@ - (void)testApplyBackdropFilterAPIChanged { // Invalid UIVisualEffectView API for "inputRadius" UIVisualEffectView* editedUIVisualEffectView2 = [[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; NSArray* subviews2 = editedUIVisualEffectView2.subviews; - for(UIView* view in subviews2) { + for (UIView* view in subviews2) { if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { - for(CIFilter* filter in view.layer.filters) { + for (CIFilter* filter in view.layer.filters) { if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { [filter setValue:@"invalidInputRadius" forKey:@"inputRadius"]; break; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 5cf990e8d566f..cdcbb397ac4c3 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -55,7 +55,7 @@ // The UIView used to extract the gaussianBlur filter. This must be a UIVisualEffectView // initalized with UIBlurEffect to extract the correct filter. Made a public property // for custom unit tests. -@property (nonatomic, strong) UIView* viewToExtractFrom; +@property(nonatomic, strong) UIView* viewToExtractFrom; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 6bd4bcf6067a4..55e5f85c8fd57 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -70,10 +70,10 @@ @implementation ChildClippingView { // Lazy initializes viewToExtractFrom as the expected UIVisualEffectView. The backdropFilter blur // requires this UIVisualEffectView initialization. The lazy initalization is only used to allow // custom unit tests. -- (UIView*) viewToExtractFrom { - if(!_viewToExtractFrom) { +- (UIView*)viewToExtractFrom { + if (!_viewToExtractFrom) { _viewToExtractFrom = [[[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; } return _viewToExtractFrom; } From 4e9da148fbae273040aeb96f0db2aae2a30dae1c Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 4 Aug 2022 09:22:25 -0700 Subject: [PATCH 32/51] Implemented Chris' last comments, reverted back to original check to call applyBlurBackdropFilters --- .../framework/Source/FlutterPlatformViews.mm | 28 ++--- .../Source/FlutterPlatformViewsTest.mm | 101 +++++++++++++++++- .../Source/FlutterPlatformViews_Internal.h | 2 +- .../Source/FlutterPlatformViews_Internal.mm | 18 ++-- 4 files changed, 121 insertions(+), 28 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index cec6aecc74307..6706876161ffa 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -406,8 +406,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; - +// int numFilters = 0; + auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { @@ -417,16 +417,16 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, - // flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(5, 5, +// flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// } break; } case kClipRect: @@ -456,10 +456,10 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { ++iter; } - if (canApplyBlurBackdrop && ([blurRadii count] > 0)) { + if(canApplyBlurBackdrop) { canApplyBlurBackdrop = [clipView applyBlurBackdropFilters:blurRadii]; } - + // 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. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 7694db9ea00f1..12385becf6c1d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -377,6 +377,97 @@ - (void)testApplyMultipleBackdropFilters { } } +- (void) testAddBackdropFilters { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + // Push a backdrop filter + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + stack.PushBackdropFilter(filter); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // + // Simulate adding 1 backdrop filter (create a new mutators stack) + // Create embedded view params + flutter::MutatorsStack stack2; + // Layer tree always pushes a screen scale factor to the stack + stack2.PushTransform(screenScaleMatrix); + // Push backdrop filters + for (int i = 0; i < 2; i++) { + stack2.PushBackdropFilter(filter); + } + + embeddedViewParams = std::make_unique(screenScaleMatrix, + SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has CAFilters for the multiple backdrop filters + XCTAssertEqual(2, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // All filters have sigma X radius + for (int i = 0; i < 2; i++) { + NSObject* gaussianFilter = childClippingView.layer.filters[i]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + } +} + - (void)testRemoveBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); @@ -896,18 +987,18 @@ - (void)testApplyBackdropFilterAPIChanged { // Each test requires a new childClippingView // Valid UIVisualEffectView API ChildClippingView* childClippingView1 = [[ChildClippingView alloc] init]; - childClippingView1.viewToExtractFrom = [[UIVisualEffectView alloc] + childClippingView1.blurEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; XCTAssertTrue([childClippingView1 applyBlurBackdropFilters:blurRadii]); // Invalid UIVisualEffectView initialization ChildClippingView* childClippingView2 = [[ChildClippingView alloc] init]; - childClippingView2.viewToExtractFrom = [[UIVisualEffectView alloc] init]; + childClippingView2.blurEffectView = [[UIVisualEffectView alloc] init]; XCTAssertFalse([childClippingView2 applyBlurBackdropFilters:blurRadii]); // Invalid UIView ChildClippingView* childClippingView3 = [[ChildClippingView alloc] init]; - childClippingView3.viewToExtractFrom = [[UIView alloc] init]; + childClippingView3.blurEffectView = [[UIView alloc] init]; XCTAssertFalse([childClippingView3 applyBlurBackdropFilters:blurRadii]); // Invalid UIVisualEffectView API for "name" @@ -927,7 +1018,7 @@ - (void)testApplyBackdropFilterAPIChanged { } ChildClippingView* childClippingView4 = [[ChildClippingView alloc] init]; - childClippingView4.viewToExtractFrom = editedUIVisualEffectView1; + childClippingView4.blurEffectView = editedUIVisualEffectView1; XCTAssertFalse([childClippingView4 applyBlurBackdropFilters:blurRadii]); // Invalid UIVisualEffectView API for "inputRadius" @@ -947,7 +1038,7 @@ - (void)testApplyBackdropFilterAPIChanged { } ChildClippingView* childClippingView5 = [[ChildClippingView alloc] init]; - childClippingView5.viewToExtractFrom = editedUIVisualEffectView2; + childClippingView5.blurEffectView = editedUIVisualEffectView2; XCTAssertFalse([childClippingView5 applyBlurBackdropFilters:blurRadii]); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index cdcbb397ac4c3..060eafe2a4bc6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -55,7 +55,7 @@ // The UIView used to extract the gaussianBlur filter. This must be a UIVisualEffectView // initalized with UIBlurEffect to extract the correct filter. Made a public property // for custom unit tests. -@property(nonatomic, strong) UIView* viewToExtractFrom; +@property(nonatomic, retain) UIView* blurEffectView; @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 55e5f85c8fd57..bdef13ce098b8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -67,15 +67,17 @@ @implementation ChildClippingView { NSObject* _gaussianFilter; } -// Lazy initializes viewToExtractFrom as the expected UIVisualEffectView. The backdropFilter blur +// Lazy initializes blurEffectView as the expected UIVisualEffectView. The backdropFilter blur // requires this UIVisualEffectView initialization. The lazy initalization is only used to allow // custom unit tests. -- (UIView*)viewToExtractFrom { - if (!_viewToExtractFrom) { - _viewToExtractFrom = [[[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; +- (UIView*)blurEffectView { + if (!_blurEffectView) { + // blurEffectView is only needed to extract its gaussianBlur filter. It is released after + // searching its subviews and extracting the filter. + _blurEffectView = [[[UIVisualEffectView alloc] + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; } - return _viewToExtractFrom; + return _blurEffectView; } // The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to @@ -95,7 +97,7 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { - (NSObject*)extractGaussianFilter { NSObject* gaussianFilter = nil; - for (UIView* view in self.viewToExtractFrom.subviews) { + for (UIView* view in self.blurEffectView.subviews) { if ([view isKindOfClass:NSClassFromString(@"_UIVisualEffectBackdropView")]) { for (CIFilter* filter in view.layer.filters) { if ([[filter valueForKey:@"name"] isEqual:@"gaussianBlur"]) { @@ -114,6 +116,7 @@ - (NSObject*)extractGaussianFilter { break; } } + [self.blurEffectView release]; return gaussianFilter; } @@ -128,7 +131,6 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { "access the gaussianBlur CAFilter."; return NO; } - [self.viewToExtractFrom release]; } BOOL updatedFilters = NO; From 2bdaec37137abfc11bd8773535caa1b9ecf78c09 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 4 Aug 2022 09:24:22 -0700 Subject: [PATCH 33/51] Formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 31 ++++++++++--------- .../Source/FlutterPlatformViewsTest.mm | 4 +-- .../Source/FlutterPlatformViews_Internal.mm | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 6706876161ffa..672fc9c30540d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -406,8 +406,8 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; - + // int numFilters = 0; + auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { @@ -417,20 +417,21 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(5, 5, -// flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// } + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, + // flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = + // @(filter.asBlur()->sigma_x()); [blurRadii + // addObject:blurRadius]; + // + // numFilters++; + // } break; } case kClipRect: - [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; + to [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; break; case kClipRRect: [maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform]; @@ -456,10 +457,10 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { ++iter; } - if(canApplyBlurBackdrop) { + if (canApplyBlurBackdrop) { canApplyBlurBackdrop = [clipView applyBlurBackdropFilters:blurRadii]; } - + // 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. diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 12385becf6c1d..85f6ea38e4b02 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -377,7 +377,7 @@ - (void)testApplyMultipleBackdropFilters { } } -- (void) testAddBackdropFilters { +- (void)testAddBackdropFilters { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); flutter::TaskRunners runners(/*label=*/self.name.UTF8String, @@ -465,7 +465,7 @@ - (void) testAddBackdropFilters { NSObject* gaussianFilter = childClippingView.layer.filters[i]; XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); - } + } } - (void)testRemoveBackdropFilters { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index bdef13ce098b8..7ebe5f7583dce 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -75,7 +75,7 @@ - (UIView*)blurEffectView { // blurEffectView is only needed to extract its gaussianBlur filter. It is released after // searching its subviews and extracting the filter. _blurEffectView = [[[UIVisualEffectView alloc] - initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; + initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]] retain]; } return _blurEffectView; } From dc064edcd126f71357baa66c57a99e56eaadac48 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 4 Aug 2022 10:06:39 -0700 Subject: [PATCH 34/51] Removed typo --- .../darwin/ios/framework/Source/FlutterPlatformViews.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 672fc9c30540d..922ec819a33b4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -431,7 +431,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { break; } case kClipRect: - to [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; + [maskView clipRect:(*iter)->GetRect() matrix:finalTransform]; break; case kClipRRect: [maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform]; From beb409004fe582fc324be8b8629643a4ade2378f Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 4 Aug 2022 10:41:05 -0700 Subject: [PATCH 35/51] Formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 922ec819a33b4..3093d5e41d21b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -406,7 +406,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -417,17 +417,17 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, - // flutter::DlTileMode::kDecal); + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(5, 5, + // flutter::DlTileMode::kDecal); // - // NSNumber* blurRadius = - // @(filter.asBlur()->sigma_x()); [blurRadii - // addObject:blurRadius]; + // NSNumber* blurRadius = + // @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; // - // numFilters++; - // } + // numFilters++; + // } break; } case kClipRect: From 559964925d6912d9ff11b4eb46812966ea5e4353 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 9 Aug 2022 08:39:10 -0700 Subject: [PATCH 36/51] Moved blurEffectView release. Working on GitHub error. --- .../ios/framework/Source/FlutterPlatformViews_Internal.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 7ebe5f7583dce..9aa47647c6bb9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -116,7 +116,7 @@ - (NSObject*)extractGaussianFilter { break; } } - [self.blurEffectView release]; +// [self.blurEffectView release]; return gaussianFilter; } @@ -126,6 +126,8 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; _gaussianFilter = [self extractGaussianFilter]; + [self.blurEffectView release]; + if (!_gaussianFilter) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " "access the gaussianBlur CAFilter."; From def8905ea9febd4683fdbc266ff265810d5eae1a Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 9 Aug 2022 08:44:39 -0700 Subject: [PATCH 37/51] Formatting checks --- .../ios/framework/Source/FlutterPlatformViews_Internal.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 9aa47647c6bb9..f868fa54f1e83 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -116,7 +116,7 @@ - (NSObject*)extractGaussianFilter { break; } } -// [self.blurEffectView release]; + // [self.blurEffectView release]; return gaussianFilter; } From d0100378ab218853cd764fb123684a614d61432d Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 9 Aug 2022 14:54:36 -0700 Subject: [PATCH 38/51] Adding gaussianFilter copies --- .../framework/Source/FlutterPlatformViews.mm | 34 ++++++------ .../Source/FlutterPlatformViews_Internal.mm | 53 +++++++++++-------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 3093d5e41d21b..c7db823ffb677 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -35,6 +35,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { namespace flutter { // Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied. BOOL canApplyBlurBackdrop = YES; +int changingBlurRadius = 0; std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( GrDirectContext* gr_context, @@ -406,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -417,17 +418,20 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(5, 5, - // flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = - // @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // } +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(changingBlurRadius, changingBlurRadius, +// flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = +// @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; +// +// changingBlurRadius++; +// } break; } case kClipRect: @@ -498,9 +502,9 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // Any UIKit related code has to run on main thread. FML_DCHECK([[NSThread currentThread] isMainThread]); // Do nothing if the view doesn't need to be composited. - if (views_to_recomposite_.count(view_id) == 0) { - return picture_recorders_[view_id]->getRecordingCanvas(); - } +// if (views_to_recomposite_.count(view_id) == 0) { +// return picture_recorders_[view_id]->getRecordingCanvas(); +// } CompositeWithParams(view_id, current_composition_params_[view_id]); views_to_recomposite_.erase(view_id); return picture_recorders_[view_id]->getRecordingCanvas(); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index f868fa54f1e83..8923a724aa604 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -116,8 +116,7 @@ - (NSObject*)extractGaussianFilter { break; } } - // [self.blurEffectView release]; - + return gaussianFilter; } @@ -134,31 +133,39 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { return NO; } } - - BOOL updatedFilters = NO; - - // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. - while ([blurRadii count] > [_activeGaussianFilters count]) { - // copy returns a deep copy of _gaussianFilter - [_activeGaussianFilters addObject:[_gaussianFilter copy]]; - updatedFilters = YES; - } - while ([blurRadii count] < [_activeGaussianFilters count]) { - [_activeGaussianFilters removeLastObject]; - updatedFilters = YES; - } - - for (NSUInteger i = 0; i < [blurRadii count]; i++) { - if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] == blurRadii[i]) { - continue; + + // Becomes YES if _activeGaussianFilters must be updated + BOOL updateActiveFilters = NO; + + if ([blurRadii count] != [_activeGaussianFilters count]) { + updateActiveFilters = YES; + + // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. + while ([blurRadii count] > [_activeGaussianFilters count]) { + // copy returns a deep copy of _gaussianFilter + [_activeGaussianFilters addObject:[_gaussianFilter copy]]; + } + while ([blurRadii count] < [_activeGaussianFilters count]) { + [_activeGaussianFilters removeLastObject]; + } + + } else { + for(NSUInteger i = 0; i < [blurRadii count]; i++) { + if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { + updateActiveFilters = YES; + break; + } } - [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; - updatedFilters = YES; } - - if (updatedFilters) { + + if(updateActiveFilters) { + for (NSUInteger i = 0; i < [blurRadii count]; i++) { + _activeGaussianFilters[i] = [_gaussianFilter copy]; + [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; + } self.layer.filters = _activeGaussianFilters; } + return YES; } From 029d7cc54e7931d1d31c0ff74f1579998ac5e22f Mon Sep 17 00:00:00 2001 From: emilyabest Date: Tue, 9 Aug 2022 15:02:45 -0700 Subject: [PATCH 39/51] Formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 36 +++++++++---------- .../Source/FlutterPlatformViews_Internal.mm | 18 +++++----- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index c7db823ffb677..fa14ecd33117d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -407,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -418,20 +418,20 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(changingBlurRadius, changingBlurRadius, -// flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = -// @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; -// -// changingBlurRadius++; -// } + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(changingBlurRadius, changingBlurRadius, + // flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = + // @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; + // + // changingBlurRadius++; + // } break; } case kClipRect: @@ -502,9 +502,9 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // Any UIKit related code has to run on main thread. FML_DCHECK([[NSThread currentThread] isMainThread]); // Do nothing if the view doesn't need to be composited. -// if (views_to_recomposite_.count(view_id) == 0) { -// return picture_recorders_[view_id]->getRecordingCanvas(); -// } + // if (views_to_recomposite_.count(view_id) == 0) { + // return picture_recorders_[view_id]->getRecordingCanvas(); + // } CompositeWithParams(view_id, current_composition_params_[view_id]); views_to_recomposite_.erase(view_id); return picture_recorders_[view_id]->getRecordingCanvas(); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 8923a724aa604..d2ab0ff8991da 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -116,7 +116,7 @@ - (NSObject*)extractGaussianFilter { break; } } - + return gaussianFilter; } @@ -133,13 +133,13 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { return NO; } } - + // Becomes YES if _activeGaussianFilters must be updated BOOL updateActiveFilters = NO; - + if ([blurRadii count] != [_activeGaussianFilters count]) { updateActiveFilters = YES; - + // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. while ([blurRadii count] > [_activeGaussianFilters count]) { // copy returns a deep copy of _gaussianFilter @@ -148,24 +148,24 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { while ([blurRadii count] < [_activeGaussianFilters count]) { [_activeGaussianFilters removeLastObject]; } - + } else { - for(NSUInteger i = 0; i < [blurRadii count]; i++) { + for (NSUInteger i = 0; i < [blurRadii count]; i++) { if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { updateActiveFilters = YES; break; } } } - - if(updateActiveFilters) { + + if (updateActiveFilters) { for (NSUInteger i = 0; i < [blurRadii count]; i++) { _activeGaussianFilters[i] = [_gaussianFilter copy]; [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } self.layer.filters = _activeGaussianFilters; } - + return YES; } From 673b64178c9fe86b1b1c8d7d63df0357cda3fdb0 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 10 Aug 2022 09:14:51 -0700 Subject: [PATCH 40/51] Simplified applyBackdropBlurFilter method --- .../framework/Source/FlutterPlatformViews.mm | 32 +++++++++---------- .../Source/FlutterPlatformViews_Internal.mm | 5 ++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index fa14ecd33117d..eb645376ae643 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -35,7 +35,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { namespace flutter { // Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied. BOOL canApplyBlurBackdrop = YES; -int changingBlurRadius = 0; +//int changingBlurRadius = 0; // TODO EMILY part of visual tests. delete before landing PR std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( GrDirectContext* gr_context, @@ -407,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; +// int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -418,20 +418,20 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(changingBlurRadius, changingBlurRadius, - // flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = - // @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; - // - // changingBlurRadius++; - // } +// if(numFilters < 1) { +// flutter::DlBlurImageFilter filter = +// flutter::DlBlurImageFilter(changingBlurRadius, changingBlurRadius, +// flutter::DlTileMode::kDecal); +// +// NSNumber* blurRadius = +// @(filter.asBlur()->sigma_x()); +// [blurRadii addObject:blurRadius]; +// +// numFilters++; +// FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; +// +// changingBlurRadius++; +// } break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index d2ab0ff8991da..1b0adc7fe8955 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -143,12 +143,11 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. while ([blurRadii count] > [_activeGaussianFilters count]) { // copy returns a deep copy of _gaussianFilter - [_activeGaussianFilters addObject:[_gaussianFilter copy]]; + [_activeGaussianFilters addObject:[[_gaussianFilter copy] autorelease] ]; } while ([blurRadii count] < [_activeGaussianFilters count]) { [_activeGaussianFilters removeLastObject]; } - } else { for (NSUInteger i = 0; i < [blurRadii count]; i++) { if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { @@ -160,7 +159,7 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { if (updateActiveFilters) { for (NSUInteger i = 0; i < [blurRadii count]; i++) { - _activeGaussianFilters[i] = [_gaussianFilter copy]; + _activeGaussianFilters[i] = [[_gaussianFilter copy] autorelease]; [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } self.layer.filters = _activeGaussianFilters; From aeef63e2de587d22aff92e36c31773e65a7ea8a1 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 10 Aug 2022 09:18:09 -0700 Subject: [PATCH 41/51] Formatting checks --- .../framework/Source/FlutterPlatformViews.mm | 32 +++++++++---------- .../Source/FlutterPlatformViews_Internal.mm | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index eb645376ae643..1a2b2063af04b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -35,7 +35,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { namespace flutter { // Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied. BOOL canApplyBlurBackdrop = YES; -//int changingBlurRadius = 0; // TODO EMILY part of visual tests. delete before landing PR +// int changingBlurRadius = 0; // TODO EMILY part of visual tests. delete before landing PR std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( GrDirectContext* gr_context, @@ -407,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR -// int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -418,20 +418,20 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR -// if(numFilters < 1) { -// flutter::DlBlurImageFilter filter = -// flutter::DlBlurImageFilter(changingBlurRadius, changingBlurRadius, -// flutter::DlTileMode::kDecal); -// -// NSNumber* blurRadius = -// @(filter.asBlur()->sigma_x()); -// [blurRadii addObject:blurRadius]; -// -// numFilters++; -// FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; -// -// changingBlurRadius++; -// } + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(changingBlurRadius, + // changingBlurRadius, flutter::DlTileMode::kDecal); + // + // NSNumber* blurRadius = + // @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; + // + // numFilters++; + // FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; + // + // changingBlurRadius++; + // } break; } case kClipRect: diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 1b0adc7fe8955..0f71fcf168054 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -143,7 +143,7 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. while ([blurRadii count] > [_activeGaussianFilters count]) { // copy returns a deep copy of _gaussianFilter - [_activeGaussianFilters addObject:[[_gaussianFilter copy] autorelease] ]; + [_activeGaussianFilters addObject:[[_gaussianFilter copy] autorelease]]; } while ([blurRadii count] < [_activeGaussianFilters count]) { [_activeGaussianFilters removeLastObject]; From b434756fa5ee5245589289b49f34bb83a5f65c7c Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 10 Aug 2022 13:23:57 -0700 Subject: [PATCH 42/51] Moved blurEffectView release to dealloc. Simplified while loops to clearing _activeGaussianFilters --- .../Source/FlutterPlatformViews_Internal.h | 5 +++-- .../Source/FlutterPlatformViews_Internal.mm | 16 +++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 060eafe2a4bc6..72798693fe216 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -48,8 +48,9 @@ // The parent view handles clipping to its subviews. @interface ChildClippingView : UIView -// Adds a blur filter to its layers. Returns NO if Apple's API has changed and -// blurred backdrop filters cannot be applied, otherwise returns YES. +// Applies blur backdrop filters to the ChildClippingView with blur radius values from +// blurRadii. Returns NO if Apple's API has changed and blurred backdrop filters cannot +// be applied, otherwise returns YES. - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii; // The UIView used to extract the gaussianBlur filter. This must be a UIVisualEffectView diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 0f71fcf168054..530cbd0bb3f56 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -125,7 +125,6 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; _gaussianFilter = [self extractGaussianFilter]; - [self.blurEffectView release]; if (!_gaussianFilter) { FML_DLOG(ERROR) << "Apple's API for UIVisualEffectView changed. Update the implementation to " @@ -139,15 +138,6 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { if ([blurRadii count] != [_activeGaussianFilters count]) { updateActiveFilters = YES; - - // Update the size of _activeGaussianFilters to match the number of applied backdrop filters. - while ([blurRadii count] > [_activeGaussianFilters count]) { - // copy returns a deep copy of _gaussianFilter - [_activeGaussianFilters addObject:[[_gaussianFilter copy] autorelease]]; - } - while ([blurRadii count] < [_activeGaussianFilters count]) { - [_activeGaussianFilters removeLastObject]; - } } else { for (NSUInteger i = 0; i < [blurRadii count]; i++) { if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { @@ -158,8 +148,10 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { } if (updateActiveFilters) { + [_activeGaussianFilters removeAllObjects]; + for (NSUInteger i = 0; i < [blurRadii count]; i++) { - _activeGaussianFilters[i] = [[_gaussianFilter copy] autorelease]; + [_activeGaussianFilters addObject: [[_gaussianFilter copy] autorelease]]; [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } self.layer.filters = _activeGaussianFilters; @@ -171,6 +163,8 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { - (void)dealloc { [_activeGaussianFilters release]; _activeGaussianFilters = nil; + + [self.blurEffectView release]; [_gaussianFilter release]; _gaussianFilter = nil; From 9429015ca504cb1098a99861fbb5887defcf382a Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 10 Aug 2022 14:59:24 -0700 Subject: [PATCH 43/51] Simplified applyBackdropBlurFilters method --- .../framework/Source/FlutterPlatformViews.mm | 31 +++++++-------- .../Source/FlutterPlatformViews_Internal.mm | 38 ++++++++----------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 1a2b2063af04b..fbfb2d6cf4412 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -407,7 +407,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; + // int numFilters = 0; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -418,20 +418,21 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // TODO EMILY: these lines are for visual simulator tests, delete before landing // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(changingBlurRadius, - // changingBlurRadius, flutter::DlTileMode::kDecal); + // if(numFilters < 1) { + // flutter::DlBlurImageFilter filter = + // flutter::DlBlurImageFilter(changingBlurRadius, + // changingBlurRadius, flutter::DlTileMode::kDecal); // - // NSNumber* blurRadius = - // @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; + // NSNumber* blurRadius = + // @(filter.asBlur()->sigma_x()); + // [blurRadii addObject:blurRadius]; // - // numFilters++; - // FML_DLOG(ERROR) << "****checkpoint***" << changingBlurRadius; + // numFilters++; + // FML_DLOG(ERROR) << "****checkpoint***" << + // changingBlurRadius; // - // changingBlurRadius++; - // } + // changingBlurRadius++; + // } break; } case kClipRect: @@ -502,9 +503,9 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { // Any UIKit related code has to run on main thread. FML_DCHECK([[NSThread currentThread] isMainThread]); // Do nothing if the view doesn't need to be composited. - // if (views_to_recomposite_.count(view_id) == 0) { - // return picture_recorders_[view_id]->getRecordingCanvas(); - // } + if (views_to_recomposite_.count(view_id) == 0) { + return picture_recorders_[view_id]->getRecordingCanvas(); + } CompositeWithParams(view_id, current_composition_params_[view_id]); views_to_recomposite_.erase(view_id); return picture_recorders_[view_id]->getRecordingCanvas(); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 530cbd0bb3f56..3edb48b99b3dc 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -57,11 +57,6 @@ void ResetAnchor(CALayer* layer) { } // namespace flutter -@interface ChildClippingView () -// The gaussianFilters currently applied to this ChildClippingView. -@property(nonatomic, retain) NSMutableArray* activeGaussianFilters; -@end - @implementation ChildClippingView { // A gaussianFilter from UIVisualEffectView that can be copied for new backdrop filters. NSObject* _gaussianFilter; @@ -121,9 +116,11 @@ - (NSObject*)extractGaussianFilter { } - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { - if (!_activeGaussianFilters) { - _activeGaussianFilters = [[[NSMutableArray alloc] init] retain]; - + // The outer if-statement checks for the first time this method is called and _gaussianFilter is + // not initialized. The inner if-statement checks if extracting the gaussianBlur was successful. + // If it was not successful, this method will not be called again. Thus the if-statements check + // for different conditions. + if (!_gaussianFilter) { _gaussianFilter = [self extractGaussianFilter]; if (!_gaussianFilter) { @@ -133,37 +130,34 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { } } - // Becomes YES if _activeGaussianFilters must be updated - BOOL updateActiveFilters = NO; + BOOL newRadiusValues = NO; - if ([blurRadii count] != [_activeGaussianFilters count]) { - updateActiveFilters = YES; + if ([blurRadii count] != [self.layer.filters count]) { + newRadiusValues = YES; } else { for (NSUInteger i = 0; i < [blurRadii count]; i++) { - if ([_activeGaussianFilters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { - updateActiveFilters = YES; + if ([self.layer.filters[i] valueForKey:@"inputRadius"] != blurRadii[i]) { + newRadiusValues = YES; break; } } } - if (updateActiveFilters) { - [_activeGaussianFilters removeAllObjects]; + if (newRadiusValues) { + NSMutableArray* newGaussianFilters = + [[[NSMutableArray alloc] initWithCapacity:[blurRadii count]] autorelease]; for (NSUInteger i = 0; i < [blurRadii count]; i++) { - [_activeGaussianFilters addObject: [[_gaussianFilter copy] autorelease]]; - [_activeGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; + newGaussianFilters[i] = [_gaussianFilter copy]; + [newGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; } - self.layer.filters = _activeGaussianFilters; + self.layer.filters = newGaussianFilters; } return YES; } - (void)dealloc { - [_activeGaussianFilters release]; - _activeGaussianFilters = nil; - [self.blurEffectView release]; [_gaussianFilter release]; From cfea8e919807fcf911175c728de67f2a3bd5cc5f Mon Sep 17 00:00:00 2001 From: emilyabest Date: Wed, 10 Aug 2022 16:43:31 -0700 Subject: [PATCH 44/51] Trying a different init method for newGaussianFilters. --- .../framework/Source/FlutterPlatformViews_Internal.mm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 3edb48b99b3dc..9d1b8f01606ef 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -144,13 +144,14 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { } if (newRadiusValues) { - NSMutableArray* newGaussianFilters = - [[[NSMutableArray alloc] initWithCapacity:[blurRadii count]] autorelease]; + NSMutableArray* newGaussianFilters = [[[NSMutableArray alloc] init] autorelease]; for (NSUInteger i = 0; i < [blurRadii count]; i++) { - newGaussianFilters[i] = [_gaussianFilter copy]; - [newGaussianFilters[i] setValue:blurRadii[i] forKey:@"inputRadius"]; + NSObject* newGaussianFilter = [_gaussianFilter copy]; + [newGaussianFilter setValue:blurRadii[i] forKey:@"inputRadius"]; + [newGaussianFilters addObject:newGaussianFilter]; } + self.layer.filters = newGaussianFilters; } From 3ed695d177845dff02cd5bce5c1a5ca2ec7edb11 Mon Sep 17 00:00:00 2001 From: emilyabest Date: Thu, 11 Aug 2022 11:13:41 -0700 Subject: [PATCH 45/51] Added autorelease after copying _gaussianFilter --- .../ios/framework/Source/FlutterPlatformViews_Internal.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 9d1b8f01606ef..52a52ec4e0100 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -147,7 +147,7 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { NSMutableArray* newGaussianFilters = [[[NSMutableArray alloc] init] autorelease]; for (NSUInteger i = 0; i < [blurRadii count]; i++) { - NSObject* newGaussianFilter = [_gaussianFilter copy]; + NSObject* newGaussianFilter = [[_gaussianFilter copy] autorelease]; [newGaussianFilter setValue:blurRadii[i] forKey:@"inputRadius"]; [newGaussianFilters addObject:newGaussianFilter]; } From a1ca9178be127532b73ef4e933da35707ff9ef63 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 12 Aug 2022 23:12:15 -0700 Subject: [PATCH 46/51] review feedbacks --- .../framework/Source/FlutterPlatformViews.mm | 2 + .../Source/FlutterPlatformViewsTest.mm | 80 +++++++++++++++++++ .../Source/FlutterPlatformViews_Internal.mm | 2 +- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 43ab8464b45d6..640545468eb9c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -539,6 +539,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { clip_count_.clear(); views_to_recomposite_.clear(); layer_pool_->RecycleLayers(); + visited_platform_views_.clear(); } SkRect FlutterPlatformViewsController::GetPlatformViewRect(int view_id) { @@ -799,6 +800,7 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { void FlutterPlatformViewsController::ResetFrameState() { slices_.clear(); composition_order_.clear(); + visited_platform_views_.clear(); } } // namespace flutter diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 85f6ea38e4b02..15a5f5d989a47 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -1096,6 +1096,86 @@ - (void)testCompositePlatformView { XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300))); } +- (void)testBackdropFilterCorrectlyPushedAndReset { + flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; + auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + auto flutterPlatformViewsController = std::make_shared(); + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*platform_views_controller=*/flutterPlatformViewsController, + /*task_runners=*/runners); + + 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::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack.PushTransform(screenScaleMatrix); + + auto embeddedViewParams = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + + flutterPlatformViewsController->BeginFrame(SkISize::Make(0, 0)); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams)); + flutterPlatformViewsController->PushVisitedPlatformView(2); + auto filter = std::make_shared(5, 2, flutter::DlTileMode::kClamp); + flutterPlatformViewsController->PushFilterToVisitedPlatformViews(filter); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + ChildClippingView* childClippingView = (ChildClippingView*)gMockPlatformView.superview.superview; + [mockFlutterView addSubview:childClippingView]; + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // childClippingView has the CAFilter, no additional filters were added + XCTAssertEqual(1, (int)[childClippingView.layer.filters count]); + // No new views were added + XCTAssertEqual(0, (int)[gMockPlatformView.subviews count]); + + // sigmaX is chosen for input radius, regardless of sigmaY + NSObject* gaussianFilter = [childClippingView.layer.filters firstObject]; + XCTAssertEqualObjects(@"gaussianBlur", [gaussianFilter valueForKey:@"name"]); + XCTAssertEqualObjects(@(5), [gaussianFilter valueForKey:@"inputRadius"]); + + // New frame, with no filter pushed. + auto embeddedViewParams2 = + std::make_unique(screenScaleMatrix, SkSize::Make(10, 10), stack); + flutterPlatformViewsController->BeginFrame(SkISize::Make(0, 0)); + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams2)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass:[ChildClippingView class]]); + + [mockFlutterView setNeedsLayout]; + [mockFlutterView layoutIfNeeded]; + + // No filter in this frame. + XCTAssertEqual(0, (int)[childClippingView.layer.filters count]); +} + - (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView { flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate; auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest"); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 52a52ec4e0100..aa2384549a045 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -159,7 +159,7 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { } - (void)dealloc { - [self.blurEffectView release]; + [_blurEffectView release]; [_gaussianFilter release]; _gaussianFilter = nil; From ee25e17e0fb6690ac5bf255204f45c79a9b7bce1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 18 Aug 2022 11:13:43 -0700 Subject: [PATCH 47/51] remove TODOs --- .../framework/Source/FlutterPlatformViews.mm | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 640545468eb9c..148120db0f894 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -35,7 +35,6 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { namespace flutter { // Becomes NO if Apple's API changes and blurred backdrop filters cannot be applied. BOOL canApplyBlurBackdrop = YES; -// int changingBlurRadius = 0; // TODO EMILY part of visual tests. delete before landing PR std::shared_ptr FlutterPlatformViewLayerPool::GetLayer( GrDirectContext* gr_context, @@ -419,33 +418,12 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { NSMutableArray* blurRadii = [[[NSMutableArray alloc] init] autorelease]; - // TODO EMILY: this line is for visual simulator tests, delete before landing PR - // int numFilters = 0; - auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { switch ((*iter)->GetType()) { case kTransform: { CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix()); finalTransform = CATransform3DConcat(transform, finalTransform); - - // TODO EMILY: these lines are for visual simulator tests, delete before landing - // PR - // if(numFilters < 1) { - // flutter::DlBlurImageFilter filter = - // flutter::DlBlurImageFilter(changingBlurRadius, - // changingBlurRadius, flutter::DlTileMode::kDecal); - // - // NSNumber* blurRadius = - // @(filter.asBlur()->sigma_x()); - // [blurRadii addObject:blurRadius]; - // - // numFilters++; - // FML_DLOG(ERROR) << "****checkpoint***" << - // changingBlurRadius; - // - // changingBlurRadius++; - // } break; } case kClipRect: From 92cc72b88317f646db3db6bf8aaadb47a014898b Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 18 Aug 2022 11:14:28 -0700 Subject: [PATCH 48/51] fixes --- .../darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index aa2384549a045..e3c3389080a7d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -160,6 +160,7 @@ - (BOOL)applyBlurBackdropFilters:(NSArray*)blurRadii { - (void)dealloc { [_blurEffectView release]; + _blurEffectView = nil; [_gaussianFilter release]; _gaussianFilter = nil; From 1dca0ddb5db7799cdcf3c9ce62ef9ab44e44069f Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 18 Aug 2022 11:24:19 -0700 Subject: [PATCH 49/51] update golden --- ...ackdrop_filter_iPhone 8_13.0_simulator.png | Bin 48543 -> 51207 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png index 2b5dff4f9c03fec2898a731e26fcde0013dd5942..110a0fc6134e08125fd7e2f3daf2446ba78cafd5 100644 GIT binary patch literal 51207 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

adoJd$(*M-OxSn)OYF(FcyHC#c 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 literal 48543 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 Date: Mon, 22 Aug 2022 15:12:10 -0700 Subject: [PATCH 50/51] Revert "Revert "Pushing BackdropFilter Mutator (#34355)" (#35543)" This reverts commit 544c34102586e4c8ea77c6b536cb726c3686e810. --- flow/embedded_views.h | 17 ++++++ flow/layers/backdrop_filter_layer.cc | 3 + flow/layers/platform_view_layer.cc | 1 + flow/mutators_stack_unittests.cc | 27 +++++++++ .../shell_test_external_view_embedder.cc | 36 +++++++++++- .../shell_test_external_view_embedder.h | 22 ++++++- shell/common/shell_unittests.cc | 58 ++++++++++++++++++- .../framework/Source/FlutterPlatformViews.mm | 9 +++ .../Source/FlutterPlatformViews_Internal.h | 9 +++ .../darwin/ios/ios_external_view_embedder.h | 7 +++ .../darwin/ios/ios_external_view_embedder.mm | 11 ++++ 11 files changed, 195 insertions(+), 5 deletions(-) diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 1839a8aa2e3f8..cc252ae527835 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -248,6 +248,11 @@ class EmbeddedViewParams { // Clippings are ignored. const SkRect& finalBoundingRect() const { return final_bounding_rect_; } + // Pushes the stored DlImageFilter object to the mutators stack. + void PushImageFilter(std::shared_ptr filter) { + mutators_stack_.PushBackdropFilter(filter); + } + // Whether the embedder should construct DisplayList objects to hold the // rendering commands for each between-view slice of the layer tree. bool display_list_enabled() const { return display_list_enabled_; } @@ -439,6 +444,18 @@ class ExternalViewEmbedder { // 'EndFrame', otherwise returns false. bool GetUsedThisFrame() const { return used_this_frame_; } + // Pushes the platform view id of a visited platform view to a list of + // visited platform views. + virtual void PushVisitedPlatformView(int64_t view_id) {} + + // Pushes a DlImageFilter object to each platform view within a list of + // visited platform views. + // + // See also: |PushVisitedPlatformView| for pushing platform view ids to the + // visited platform views list. + virtual void PushFilterToVisitedPlatformViews( + std::shared_ptr filter) {} + private: bool used_this_frame_ = false; diff --git a/flow/layers/backdrop_filter_layer.cc b/flow/layers/backdrop_filter_layer.cc index d52ac787c44c0..9f67b746b1c9b 100644 --- a/flow/layers/backdrop_filter_layer.cc +++ b/flow/layers/backdrop_filter_layer.cc @@ -43,6 +43,9 @@ void BackdropFilterLayer::Preroll(PrerollContext* context, const SkMatrix& matrix) { Layer::AutoPrerollSaveLayerState save = Layer::AutoPrerollSaveLayerState::Create(context, true, bool(filter_)); + if (context->view_embedder != nullptr) { + context->view_embedder->PushFilterToVisitedPlatformViews(filter_); + } SkRect child_paint_bounds = SkRect::MakeEmpty(); PrerollChildren(context, matrix, &child_paint_bounds); child_paint_bounds.join(context->cull_rect); diff --git a/flow/layers/platform_view_layer.cc b/flow/layers/platform_view_layer.cc index 8a46cbbce127c..a86886fb1b04f 100644 --- a/flow/layers/platform_view_layer.cc +++ b/flow/layers/platform_view_layer.cc @@ -29,6 +29,7 @@ void PlatformViewLayer::Preroll(PrerollContext* context, context->display_list_enabled); context->view_embedder->PrerollCompositeEmbeddedView(view_id_, std::move(params)); + context->view_embedder->PushVisitedPlatformView(view_id_); } void PlatformViewLayer::Paint(PaintContext& context) const { diff --git a/flow/mutators_stack_unittests.cc b/flow/mutators_stack_unittests.cc index a460125ef9627..c93838cfa68bf 100644 --- a/flow/mutators_stack_unittests.cc +++ b/flow/mutators_stack_unittests.cc @@ -163,6 +163,8 @@ TEST(MutatorsStack, Equality) { stack.PushClipPath(path); int alpha = 240; stack.PushOpacity(alpha); + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + stack.PushBackdropFilter(filter); MutatorsStack stackOther; SkMatrix matrixOther = SkMatrix::Scale(1, 1); @@ -175,6 +177,9 @@ TEST(MutatorsStack, Equality) { stackOther.PushClipPath(otherPath); int otherAlpha = 240; stackOther.PushOpacity(otherAlpha); + auto otherFilter = + std::make_shared(5, 5, DlTileMode::kClamp); + stackOther.PushBackdropFilter(otherFilter); ASSERT_TRUE(stack == stackOther); } @@ -204,6 +209,11 @@ TEST(Mutator, Initialization) { int alpha = 240; Mutator mutator5 = Mutator(alpha); ASSERT_TRUE(mutator5.GetType() == MutatorType::kOpacity); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + Mutator mutator6 = Mutator(filter); + ASSERT_TRUE(mutator6.GetType() == MutatorType::kBackdropFilter); + ASSERT_TRUE(mutator6.GetFilter() == *filter); } TEST(Mutator, CopyConstructor) { @@ -232,6 +242,11 @@ TEST(Mutator, CopyConstructor) { Mutator mutator5 = Mutator(alpha); Mutator copy5 = Mutator(mutator5); ASSERT_TRUE(mutator5 == copy5); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + Mutator mutator6 = Mutator(filter); + Mutator copy6 = Mutator(mutator6); + ASSERT_TRUE(mutator6 == copy6); } TEST(Mutator, Equality) { @@ -260,6 +275,11 @@ TEST(Mutator, Equality) { Mutator mutator5 = Mutator(alpha); Mutator otherMutator5 = Mutator(alpha); ASSERT_TRUE(mutator5 == otherMutator5); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + Mutator mutator6 = Mutator(filter); + Mutator otherMutator6 = Mutator(filter); + ASSERT_TRUE(mutator6 == otherMutator6); } TEST(Mutator, UnEquality) { @@ -275,6 +295,13 @@ TEST(Mutator, UnEquality) { Mutator mutator2 = Mutator(alpha); Mutator otherMutator2 = Mutator(alpha2); ASSERT_TRUE(mutator2 != otherMutator2); + + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + auto filter2 = + std::make_shared(10, 10, DlTileMode::kClamp); + Mutator mutator3 = Mutator(filter); + Mutator otherMutator3 = Mutator(filter2); + ASSERT_TRUE(mutator3 != otherMutator3); } } // namespace testing diff --git a/shell/common/shell_test_external_view_embedder.cc b/shell/common/shell_test_external_view_embedder.cc index a366e6f99075c..579f682089517 100644 --- a/shell/common/shell_test_external_view_embedder.cc +++ b/shell/common/shell_test_external_view_embedder.cc @@ -28,6 +28,14 @@ SkISize ShellTestExternalViewEmbedder::GetLastSubmittedFrameSize() { return last_submitted_frame_size_; } +std::vector ShellTestExternalViewEmbedder::GetVisitedPlatformViews() { + return visited_platform_views_; +} + +MutatorsStack ShellTestExternalViewEmbedder::GetStack(int64_t view_id) { + return mutators_stacks_[view_id]; +} + // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::CancelFrame() {} @@ -41,7 +49,16 @@ void ShellTestExternalViewEmbedder::BeginFrame( // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::PrerollCompositeEmbeddedView( int view_id, - std::unique_ptr params) {} + std::unique_ptr params) { + SkRect view_bounds = SkRect::Make(frame_size_); + std::unique_ptr view; + if (params->display_list_enabled()) { + view = std::make_unique(view_bounds); + } else { + view = std::make_unique(view_bounds); + } + slices_.insert_or_assign(view_id, std::move(view)); +} // |ExternalViewEmbedder| PostPrerollResult ShellTestExternalViewEmbedder::PostPrerollAction( @@ -62,9 +79,24 @@ ShellTestExternalViewEmbedder::GetCurrentBuilders() { } // |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::PushVisitedPlatformView(int64_t view_id) { + visited_platform_views_.push_back(view_id); +} + +// |ExternalViewEmbedder| +void ShellTestExternalViewEmbedder::PushFilterToVisitedPlatformViews( + std::shared_ptr filter) { + for (int64_t id : visited_platform_views_) { + EmbeddedViewParams params = current_composition_params_[id]; + params.PushImageFilter(filter); + current_composition_params_[id] = params; + mutators_stacks_[id] = params.mutatorsStack(); + } +} + EmbedderPaintContext ShellTestExternalViewEmbedder::CompositeEmbeddedView( int view_id) { - return {nullptr, nullptr}; + return {slices_[view_id]->canvas(), slices_[view_id]->builder()}; } // |ExternalViewEmbedder| diff --git a/shell/common/shell_test_external_view_embedder.h b/shell/common/shell_test_external_view_embedder.h index 1bb70fd131e07..583a09182e5fc 100644 --- a/shell/common/shell_test_external_view_embedder.h +++ b/shell/common/shell_test_external_view_embedder.h @@ -7,6 +7,7 @@ #include "flutter/flow/embedded_views.h" #include "flutter/fml/raster_thread_merger.h" +#include "third_party/skia/include/core/SkPictureRecorder.h" namespace flutter { @@ -32,9 +33,15 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // the external view embedder. int GetSubmittedFrameCount(); - // Returns the size of last submitted frame surface + // Returns the size of last submitted frame surface. SkISize GetLastSubmittedFrameSize(); + // Returns the mutators stack for the given platform view. + MutatorsStack GetStack(int64_t); + + // Returns the list of visited platform views. + std::vector GetVisitedPlatformViews(); + private: // |ExternalViewEmbedder| void CancelFrame() override; @@ -64,6 +71,13 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // |ExternalViewEmbedder| EmbedderPaintContext CompositeEmbeddedView(int view_id) override; + // |ExternalViewEmbedder| + void PushVisitedPlatformView(int64_t view_id) override; + + // |ExternalViewEmbedder| + void PushFilterToVisitedPlatformViews( + std::shared_ptr filter) override; + // |ExternalViewEmbedder| void SubmitFrame(GrDirectContext* context, std::unique_ptr frame) override; @@ -84,7 +98,11 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { PostPrerollResult post_preroll_result_; bool support_thread_merging_; - + SkISize frame_size_; + std::map> slices_; + std::map mutators_stacks_; + std::map current_composition_params_; + std::vector visited_platform_views_; std::atomic submitted_frame_count_; std::atomic last_submitted_frame_size_; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index e2193d9be7d24..5d513b5239541 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -13,8 +13,10 @@ #include "assets/directory_asset_bundle.h" #include "common/graphics/persistent_cache.h" +#include "flutter/flow/layers/backdrop_filter_layer.h" #include "flutter/flow/layers/display_list_layer.h" #include "flutter/flow/layers/layer_raster_cache_item.h" +#include "flutter/flow/layers/platform_view_layer.h" #include "flutter/flow/layers/transform_layer.h" #include "flutter/fml/command_line.h" #include "flutter/fml/dart/dart_converter.h" @@ -765,12 +767,66 @@ TEST_F(ShellTest, ExternalEmbedderNoThreadMerger) { PumpOneFrame(shell.get(), 100, 100, builder); end_frame_latch.Wait(); - ASSERT_TRUE(end_frame_called); DestroyShell(std::move(shell)); } +TEST_F(ShellTest, PushBackdropFilterToVisitedPlatformViews) { + auto settings = CreateSettingsForFixture(); + fml::AutoResetWaitableEvent end_frame_latch; + bool end_frame_called = false; + auto end_frame_callback = + [&](bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { + ASSERT_TRUE(raster_thread_merger.get() == nullptr); + ASSERT_FALSE(should_resubmit_frame); + end_frame_called = true; + end_frame_latch.Signal(); + }; + auto external_view_embedder = std::make_shared( + end_frame_callback, PostPrerollResult::kResubmitFrame, false); + auto shell = CreateShell(std::move(settings), GetTaskRunnersForFixture(), + false, external_view_embedder); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + + LayerTreeBuilder builder = [&](std::shared_ptr root) { + auto platform_view_layer = std::make_shared( + SkPoint::Make(10, 10), SkSize::Make(10, 10), 50); + root->Add(platform_view_layer); + auto filter = std::make_shared(5, 5, DlTileMode::kClamp); + auto backdrop_filter_layer = + std::make_shared(filter, DlBlendMode::kSrcOver); + root->Add(backdrop_filter_layer); + auto platform_view_layer2 = std::make_shared( + SkPoint::Make(10, 10), SkSize::Make(10, 10), 75); + backdrop_filter_layer->Add(platform_view_layer2); + }; + + PumpOneFrame(shell.get(), 100, 100, builder); + end_frame_latch.Wait(); + ASSERT_EQ(external_view_embedder->GetVisitedPlatformViews().size(), + (const unsigned long)2); + ASSERT_EQ(external_view_embedder->GetVisitedPlatformViews()[0], 50); + ASSERT_EQ(external_view_embedder->GetVisitedPlatformViews()[1], 75); + ASSERT_TRUE(external_view_embedder->GetStack(75).is_empty()); + ASSERT_FALSE(external_view_embedder->GetStack(50).is_empty()); + + auto filter = DlBlurImageFilter(5, 5, DlTileMode::kClamp); + auto mutator = *external_view_embedder->GetStack(50).Begin(); + ASSERT_EQ(mutator->GetType(), MutatorType::kBackdropFilter); + ASSERT_EQ(mutator->GetFilter(), filter); + + DestroyShell(std::move(shell)); +} + // TODO(https://github.com/flutter/flutter/issues/59816): Enable on fuchsia. TEST_F(ShellTest, #if defined(OS_FUCHSIA) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 107cdadad9871..bc8426c9b5085 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -320,6 +320,15 @@ - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { } } +void FlutterPlatformViewsController::PushFilterToVisitedPlatformViews( + std::shared_ptr filter) { + for (int64_t id : visited_platform_views_) { + EmbeddedViewParams params = current_composition_params_[id]; + params.PushImageFilter(filter); + current_composition_params_[id] = params; + } +} + void FlutterPlatformViewsController::PrerollCompositeEmbeddedView( int view_id, std::unique_ptr params) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index c3d8fa87e3d7d..e9242d035ce0d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -196,6 +196,12 @@ class FlutterPlatformViewsController { // responder. Returns -1 if no such platform view is found. long FindFirstResponderPlatformViewId(); + // Pushes backdrop filter mutation to the mutator stack of each visited platform view. + void PushFilterToVisitedPlatformViews(std::shared_ptr filter); + + // Pushes the view id of a visted platform view to the list of visied platform views. + void PushVisitedPlatformView(int64_t view_id) { visited_platform_views_.push_back(view_id); } + private: static const size_t kMaxLayerAllocations = 2; @@ -303,6 +309,9 @@ class FlutterPlatformViewsController { // The last ID in this vector belond to the that is composited on top of all others. std::vector composition_order_; + // A vector of visited platform view IDs. + std::vector visited_platform_views_; + // The latest composition order that was presented in Present(). std::vector active_composition_order_; diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.h b/shell/platform/darwin/ios/ios_external_view_embedder.h index 3a5a0b3c17613..03cec910bae39 100644 --- a/shell/platform/darwin/ios/ios_external_view_embedder.h +++ b/shell/platform/darwin/ios/ios_external_view_embedder.h @@ -67,6 +67,13 @@ class IOSExternalViewEmbedder : public ExternalViewEmbedder { // |ExternalViewEmbedder| bool SupportsDynamicThreadMerging() override; + // |ExternalViewEmbedder| + void PushFilterToVisitedPlatformViews( + std::shared_ptr filter) override; + + // |ExternalViewEmbedder| + void PushVisitedPlatformView(int64_t view_id) override; + FML_DISALLOW_COPY_AND_ASSIGN(IOSExternalViewEmbedder); }; diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.mm b/shell/platform/darwin/ios/ios_external_view_embedder.mm index e67983321c770..76995be2c56ad 100644 --- a/shell/platform/darwin/ios/ios_external_view_embedder.mm +++ b/shell/platform/darwin/ios/ios_external_view_embedder.mm @@ -98,4 +98,15 @@ return true; } +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushFilterToVisitedPlatformViews( + std::shared_ptr filter) { + platform_views_controller_->PushFilterToVisitedPlatformViews(filter); +} + +// |ExternalViewEmbedder| +void IOSExternalViewEmbedder::PushVisitedPlatformView(int64_t view_id) { + platform_views_controller_->PushVisitedPlatformView(view_id); +} + } // namespace flutter From fd249048c4c31c9038933f611fec0875734248ba Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 30 Aug 2022 10:47:56 -0700 Subject: [PATCH 51/51] update golden --- ...ackdrop_filter_iPhone 8_13.0_simulator.png | Bin 51207 -> 49083 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_iPhone 8_13.0_simulator.png index 110a0fc6134e08125fd7e2f3daf2446ba78cafd5..307864c09a863c7b3b8e696d7c7c63a76968ab25 100644 GIT binary patch literal 49083 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

o|MZ2cpLm>2Wo(}Rn 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 KxD-1F9{Cp+j9N1Q literal 51207 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

adoJd$(*M-OxSn)OYF(FcyHC#c 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