diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 51349cb70e1ff..70fdf7936bf8d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -18,8 +18,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/ios_surface.h" -static const NSUInteger kFlutterClippingMaskViewPoolCapacity = 5; - @implementation UIView (FirstResponder) - (BOOL)flt_hasFirstResponderInViewHierarchySubtree { if (self.isFirstResponder) { @@ -447,6 +445,8 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, clipView.maskView = [mask_view_pool_.get() getMaskViewWithFrame:frame]; } +// This method is only called when the `embedded_view` needs to be re-composited at the current +// frame. See: `CompositeWithParams` for details. void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view, const SkRect& bounding_rect) { @@ -461,12 +461,10 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, NSMutableArray* blurFilters = [[[NSMutableArray alloc] init] autorelease]; FML_DCHECK(!clipView.maskView || [clipView.maskView isKindOfClass:[FlutterClippingMaskView class]]); - if (mask_view_pool_.get() == nil) { - mask_view_pool_.reset([[FlutterClippingMaskViewPool alloc] - initWithCapacity:kFlutterClippingMaskViewPoolCapacity]); + if (clipView.maskView) { + [mask_view_pool_.get() insertViewToPoolIfNeeded:(FlutterClippingMaskView*)(clipView.maskView)]; + clipView.maskView = nil; } - [mask_view_pool_.get() recycleMaskViews]; - clipView.maskView = nil; CGFloat screenScale = [UIScreen mainScreen].scale; auto iter = mutators_stack.Begin(); while (iter != mutators_stack.End()) { @@ -570,6 +568,14 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, embedded_view.layer.transform = flutter::GetCATransform3DFromSkMatrix(transformMatrix); } +// Composite the PlatformView with `view_id`. +// +// Every frame, during the paint traversal of the layer tree, this method is called for all +// the PlatformViews in `views_to_recomposite_`. +// +// Note that `views_to_recomposite_` does not represent all the views in the view hierarchy, +// if a PlatformView does not change its composition parameter from last frame, it is not +// included in the `views_to_recomposite_`. void FlutterPlatformViewsController::CompositeWithParams(int64_t view_id, const EmbeddedViewParams& params) { CGRect frame = CGRectMake(0, 0, params.sizePoints().width(), params.sizePoints().height()); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index fd656a9d1767f..01aab6b5478e0 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -2649,12 +2649,15 @@ - (void)testFlutterClippingMaskViewPoolReuseViewsAfterRecycle { [[[FlutterClippingMaskViewPool alloc] initWithCapacity:2] autorelease]; FlutterClippingMaskView* view1 = [pool getMaskViewWithFrame:CGRectZero]; FlutterClippingMaskView* view2 = [pool getMaskViewWithFrame:CGRectZero]; - [pool recycleMaskViews]; + [pool insertViewToPoolIfNeeded:view1]; + [pool insertViewToPoolIfNeeded:view2]; CGRect newRect = CGRectMake(0, 0, 10, 10); FlutterClippingMaskView* view3 = [pool getMaskViewWithFrame:newRect]; FlutterClippingMaskView* view4 = [pool getMaskViewWithFrame:newRect]; - XCTAssertEqual(view1, view3); - XCTAssertEqual(view2, view4); + // view3 and view4 should randomly get either of view1 and view2. + NSSet* set1 = [NSSet setWithObjects:view1, view2, nil]; + NSSet* set2 = [NSSet setWithObjects:view3, view4, nil]; + XCTAssertEqualObjects(set1, set2); XCTAssertTrue(CGRectEqualToRect(view3.frame, newRect)); XCTAssertTrue(CGRectEqualToRect(view4.frame, newRect)); } @@ -2727,10 +2730,6 @@ - (void)testClipMaskViewIsReused { auto embeddedViewParams1 = std::make_unique( screenScaleMatrix, SkSize::Make(10, 10), stack1); - flutter::MutatorsStack stack2; - auto embeddedViewParams2 = std::make_unique( - screenScaleMatrix, SkSize::Make(10, 10), stack2); - flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1)); flutterPlatformViewsController->CompositeEmbeddedView(1); UIView* childClippingView1 = gMockPlatformView.superview.superview; @@ -2738,6 +2737,10 @@ - (void)testClipMaskViewIsReused { XCTAssertNotNil(maskView1); // Composite a new frame. + flutterPlatformViewsController->BeginFrame(SkISize::Make(100, 100)); + flutter::MutatorsStack stack2; + auto embeddedViewParams2 = std::make_unique( + screenScaleMatrix, SkSize::Make(10, 10), stack2); auto embeddedViewParams3 = std::make_unique( screenScaleMatrix, SkSize::Make(10, 10), stack2); flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams3)); @@ -2763,6 +2766,77 @@ - (void)testClipMaskViewIsReused { XCTAssertNil(childClippingView1.maskView); } +- (void)testDifferentClipMaskViewIsUsedForEachView { + 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" : @1, @"viewType" : @"MockFlutterPlatformView"}], + result); + UIView* view1 = gMockPlatformView; + + // This overwrites `gMockPlatformView` to another view. + flutterPlatformViewsController->OnMethodCall( + [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}], + result); + UIView* view2 = gMockPlatformView; + + XCTAssertNotNil(gMockPlatformView); + UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)] autorelease]; + flutterPlatformViewsController->SetFlutterView(mockFlutterView); + // Create embedded view params + flutter::MutatorsStack stack1; + // Layer tree always pushes a screen scale factor to the stack + SkMatrix screenScaleMatrix = + SkMatrix::Scale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale); + stack1.PushTransform(screenScaleMatrix); + // Push a clip rect + SkRect rect = SkRect::MakeXYWH(2, 2, 3, 3); + stack1.PushClipRect(rect); + + auto embeddedViewParams1 = std::make_unique( + screenScaleMatrix, SkSize::Make(10, 10), stack1); + + flutter::MutatorsStack stack2; + stack2.PushClipRect(rect); + auto embeddedViewParams2 = std::make_unique( + screenScaleMatrix, SkSize::Make(10, 10), stack2); + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(1, std::move(embeddedViewParams1)); + flutterPlatformViewsController->CompositeEmbeddedView(1); + UIView* childClippingView1 = view1.superview.superview; + + flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams2)); + flutterPlatformViewsController->CompositeEmbeddedView(2); + UIView* childClippingView2 = view2.superview.superview; + UIView* maskView1 = childClippingView1.maskView; + UIView* maskView2 = childClippingView2.maskView; + XCTAssertNotEqual(maskView1, maskView2); +} + // Return true if a correct visual effect view is found. It also implies all the validation in this // method passes. // diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 8447d19ca2df9..55e9d5a8fc46c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -56,7 +56,7 @@ // in the pool. If there are none available, a new FlutterClippingMaskView is constructed. If the // capacity is reached, the newly constructed FlutterClippingMaskView is not added to the pool. // -// Call |recycleMaskViews| to mark all the FlutterClippingMaskViews in the pool available. +// Call |insertViewToPoolIfNeeded:| to return a maskView to the pool. @interface FlutterClippingMaskViewPool : NSObject // Initialize the pool with `capacity`. When the `capacity` is reached, a FlutterClippingMaskView is @@ -66,8 +66,8 @@ // Reuse a maskView from the pool, or allocate a new one. - (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame; -// Mark all the maskViews available. -- (void)recycleMaskViews; +// Insert the `maskView` into the pool. +- (void)insertViewToPoolIfNeeded:(FlutterClippingMaskView*)maskView; @end @@ -291,20 +291,12 @@ class FlutterPlatformViewsController { int CountClips(const MutatorsStack& mutators_stack); void ClipViewSetMaskView(UIView* clipView); + // Applies the mutators in the mutators_stack to the UIView chain that was constructed by // `ReconstructClipViewsChain` // - // Clips are applied to the super view with a CALayer mask. Transforms are applied to the - // current view that's at the head of the chain. For example the following mutators stack [T_1, - // C_2, T_3, T_4, C_5, T_6] where T denotes a transform and C denotes a clip, will result in the - // following UIView tree: - // - // C_2 -> C_5 -> PLATFORM_VIEW - // (PLATFORM_VIEW is a subview of C_5 which is a subview of C_2) - // - // T_1 is applied to C_2, T_3 and T_4 are applied to C_5, and T_6 is applied to PLATFORM_VIEW. - // - // After each clip operation, we update the head to the super view of the current head. + // Clips are applied to the `embedded_view`'s super view(|ChildClippingView|) using a + // |FlutterClippingMaskView|. Transforms are applied to `embedded_view` // // The `bounding_rect` is the final bounding rect of the PlatformView // (EmbeddedViewParams::finalBoundingRect). If a clip mutator's rect contains the final bounding @@ -312,6 +304,7 @@ class FlutterPlatformViewsController { void ApplyMutators(const MutatorsStack& mutators_stack, UIView* embedded_view, const SkRect& bounding_rect); + void CompositeWithParams(int64_t view_id, const EmbeddedViewParams& params); // Allocates a new FlutterPlatformViewLayer if needed, draws the pixels within the rect from diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm index 89e1687795248..ebcfcad0ddfc1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm @@ -9,6 +9,7 @@ #import "flutter/shell/platform/darwin/ios/ios_surface.h" static int kMaxPointsInVerb = 4; +static const NSUInteger kFlutterClippingMaskViewPoolCapacity = 5; namespace flutter { @@ -26,7 +27,10 @@ FlutterPlatformViewsController::FlutterPlatformViewsController() : layer_pool_(std::make_unique()), - weak_factory_(std::make_unique>(this)){}; + weak_factory_(std::make_unique>(this)) { + mask_view_pool_.reset( + [[FlutterClippingMaskViewPool alloc] initWithCapacity:kFlutterClippingMaskViewPoolCapacity]); +}; FlutterPlatformViewsController::~FlutterPlatformViewsController() = default; @@ -458,9 +462,10 @@ @interface FlutterClippingMaskViewPool () // The maximum number of `FlutterClippingMaskView` the pool can contain. // This prevents the pool to grow infinately and limits the maximum memory a pool can use. @property(assign, nonatomic) NSUInteger capacity; -@property(retain, nonatomic) NSMutableArray* pool; -// The index points to the first available FlutterClippingMaskView in the `pool`. -@property(assign, nonatomic) NSUInteger availableIndex; + +// The pool contains the views that are available to use. +// The number of items in the pool must not excceds `capacity`. +@property(retain, nonatomic) NSMutableSet* pool; @end @@ -468,48 +473,42 @@ @implementation FlutterClippingMaskViewPool : NSObject - (instancetype)initWithCapacity:(NSInteger)capacity { if (self = [super init]) { - _pool = [[NSMutableArray alloc] initWithCapacity:capacity]; + // Most of cases, there are only one PlatformView in the scene. + // Thus init with the capacity of 1. + _pool = [[NSMutableSet alloc] initWithCapacity:1]; _capacity = capacity; - _availableIndex = 0; } return self; } - (FlutterClippingMaskView*)getMaskViewWithFrame:(CGRect)frame { - FML_DCHECK(self.availableIndex <= self.capacity); + FML_DCHECK(self.pool.count <= self.capacity); FlutterClippingMaskView* maskView; - if (self.availableIndex == self.capacity) { - // The pool is full, alloc a new one. + if (self.pool.count == 0) { + // The pool is empty, alloc a new one. maskView = [[[FlutterClippingMaskView alloc] initWithFrame:frame screenScale:[UIScreen mainScreen].scale] autorelease]; return maskView; } - - if (self.availableIndex >= self.pool.count) { - // The pool doesn't have enough maskViews, alloc a new one and add to the pool. - maskView = - [[[FlutterClippingMaskView alloc] initWithFrame:frame - screenScale:[UIScreen mainScreen].scale] autorelease]; - [self.pool addObject:maskView]; - FML_DCHECK(self.pool.count <= self.capacity); - } else { - // Reuse a maskView from the pool. - maskView = [self.pool objectAtIndex:self.availableIndex]; - maskView.frame = frame; - [maskView reset]; - } - self.availableIndex++; + maskView = [self.pool anyObject]; + maskView.frame = frame; + [maskView reset]; + [self.pool removeObject:maskView]; return maskView; } -- (void)recycleMaskViews { - self.availableIndex = 0; +- (void)insertViewToPoolIfNeeded:(FlutterClippingMaskView*)maskView { + FML_DCHECK(![self.pool containsObject:maskView]); + FML_DCHECK(self.pool.count <= self.capacity); + if (self.pool.count == self.capacity) { + return; + } + [self.pool addObject:maskView]; } - (void)dealloc { [_pool release]; - _pool = nil; [super dealloc]; } diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 0fcb3c83f3e1f..4e69202318806 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -57,6 +57,9 @@ 684FFF8D29F9C10700281002 /* golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 684FFF7929F9C10600281002 /* golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_16.2_simulator.png */; }; 684FFF8E29F9C10700281002 /* golden_spawn_engine_works_iPhone SE (3rd generation)_16.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 684FFF7A29F9C10700281002 /* golden_spawn_engine_works_iPhone SE (3rd generation)_16.2_simulator.png */; }; 684FFF8F29F9C10700281002 /* golden_platform_view_with_other_backdrop_filter_iPhone SE (3rd generation)_16.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 684FFF7B29F9C10700281002 /* golden_platform_view_with_other_backdrop_filter_iPhone SE (3rd generation)_16.2_simulator.png */; }; + 6860CE252A01B2FF00B68EC5 /* golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6860CE222A01B2FF00B68EC5 /* golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png */; }; + 6860CE262A01B2FF00B68EC5 /* golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6860CE232A01B2FF00B68EC5 /* golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png */; }; + 6860CE272A01B2FF00B68EC5 /* golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6860CE242A01B2FF00B68EC5 /* golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png */; }; 68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */; }; 68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 68D4017C2564859300ECD91A /* ContinuousTexture.m */; }; F26F15B8268B6B5600EC54D3 /* iPadGestureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */; }; @@ -174,6 +177,9 @@ 684FFF7929F9C10600281002 /* golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_16.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_16.2_simulator.png"; sourceTree = ""; }; 684FFF7A29F9C10700281002 /* golden_spawn_engine_works_iPhone SE (3rd generation)_16.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_spawn_engine_works_iPhone SE (3rd generation)_16.2_simulator.png"; sourceTree = ""; }; 684FFF7B29F9C10700281002 /* golden_platform_view_with_other_backdrop_filter_iPhone SE (3rd generation)_16.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_with_other_backdrop_filter_iPhone SE (3rd generation)_16.2_simulator.png"; sourceTree = ""; }; + 6860CE222A01B2FF00B68EC5 /* golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png"; sourceTree = ""; }; + 6860CE232A01B2FF00B68EC5 /* golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png"; sourceTree = ""; }; + 6860CE242A01B2FF00B68EC5 /* golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png"; sourceTree = ""; }; 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGestureRecognizerTests.m; sourceTree = ""; }; 68D4017B2564859300ECD91A /* ContinuousTexture.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContinuousTexture.h; sourceTree = ""; }; 68D4017C2564859300ECD91A /* ContinuousTexture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContinuousTexture.m; sourceTree = ""; }; @@ -302,6 +308,9 @@ F7B464DC2759D02B00079189 /* Goldens */ = { isa = PBXGroup; children = ( + 6860CE242A01B2FF00B68EC5 /* golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png */, + 6860CE232A01B2FF00B68EC5 /* golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png */, + 6860CE222A01B2FF00B68EC5 /* golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png */, 684FFF7229F9C10500281002 /* golden_bogus_font_text_iPhone SE (3rd generation)_16.2_simulator.png */, 684FFF7029F9C10500281002 /* golden_non_full_screen_flutter_view_platform_view_iPhone SE (3rd generation)_16.2_simulator.png */, 684FFF7729F9C10600281002 /* golden_platform_view_clippath_iPhone SE (3rd generation)_16.2_simulator.png */, @@ -456,7 +465,9 @@ 684FFF7D29F9C10700281002 /* golden_platform_view_cliprect_with_transform_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8829F9C10700281002 /* golden_platform_view_cliprect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8529F9C10700281002 /* golden_platform_view_cliprrect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, + 6860CE252A01B2FF00B68EC5 /* golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8F29F9C10700281002 /* golden_platform_view_with_other_backdrop_filter_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, + 6860CE262A01B2FF00B68EC5 /* golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8A29F9C10700281002 /* golden_platform_view_clippath_with_transform_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8429F9C10700281002 /* golden_non_full_screen_flutter_view_platform_view_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF7C29F9C10700281002 /* golden_platform_view_cliprrect_with_transform_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, @@ -469,6 +480,7 @@ 684FFF8D29F9C10700281002 /* golden_platform_view_large_cliprrect_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8329F9C10700281002 /* golden_platform_view_transform_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8B29F9C10700281002 /* golden_platform_view_clippath_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, + 6860CE272A01B2FF00B68EC5 /* golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, 684FFF8129F9C10700281002 /* golden_platform_view_large_cliprrect_with_transform_iPhone SE (3rd generation)_16.2_simulator.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 28c84f8e6896f..4dca96551a3b7 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -69,7 +69,10 @@ - (BOOL)application:(UIApplication*)application @"--spawn-engine-works" : @"spawn_engine_works", @"--pointer-events" : @"pointer_events", @"--platform-view-scrolling-under-widget" : @"platform_view_scrolling_under_widget", - @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved" + @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved", + @"--two-platform-view-clip-rect" : @"two_platform_view_clip_rect", + @"--two-platform-view-clip-rrect" : @"two_platform_view_clip_rrect", + @"--two-platform-view-clip-path" : @"two_platform_view_clip_path", }; __block NSString* flutterViewControllerTestName = nil; [launchArgsMap diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m index 6ecb07a7c7c5d..3bcff696844a0 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenTestManager.m @@ -47,6 +47,9 @@ - (instancetype)initWithLaunchArg:(NSString*)launchArg { @"--bogus-font-text" : @"bogus_font_text", @"--spawn-engine-works" : @"spawn_engine_works", @"--platform-view-cliprect-after-moved" : @"platform_view_cliprect_after_moved", + @"--two-platform-view-clip-rect" : @"two_platform_view_clip_rect", + @"--two-platform-view-clip-rrect" : @"two_platform_view_clip_rrect", + @"--two-platform-view-clip-path" : @"two_platform_view_clip_path", }; }); _identifier = launchArgsMap[launchArg]; diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m index 47d49f3c7779c..cd28f98781440 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m @@ -254,6 +254,60 @@ - (void)testPlatformView { @end +@interface TwoPlatformViewClipRectTests : GoldenPlatformViewTests + +@end + +@implementation TwoPlatformViewClipRectTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + GoldenTestManager* manager = + [[GoldenTestManager alloc] initWithLaunchArg:@"--two-platform-view-clip-rect"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkPlatformViewGolden]; +} + +@end + +@interface TwoPlatformViewClipRRectTests : GoldenPlatformViewTests + +@end + +@implementation TwoPlatformViewClipRRectTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + GoldenTestManager* manager = + [[GoldenTestManager alloc] initWithLaunchArg:@"--two-platform-view-clip-rrect"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkPlatformViewGolden]; +} + +@end + +@interface TwoPlatformViewClipPathTests : GoldenPlatformViewTests + +@end + +@implementation TwoPlatformViewClipPathTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + GoldenTestManager* manager = + [[GoldenTestManager alloc] initWithLaunchArg:@"--two-platform-view-clip-path"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkPlatformViewGolden]; +} + +@end + @interface PlatformViewMutationTransformTests : GoldenPlatformViewTests @end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png new file mode 100644 index 0000000000000..71caaac361f03 Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_iPhone SE (3rd generation)_16.2_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png new file mode 100644 index 0000000000000..d90daed50411a Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_iPhone SE (3rd generation)_16.2_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png new file mode 100644 index 0000000000000..09d187affdd99 Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_iPhone SE (3rd generation)_16.2_simulator.png differ diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 75ccd395206be..741ed0c854cef 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -112,7 +112,6 @@ class PlatformViewNoOverlayIntersectionScenario extends Scenario } } - /// A platform view that is larger than the display size. /// This is only applicable on Android while using virtual displays. /// Related issue: https://github.com/flutter/flutter/issues/28978. @@ -576,7 +575,7 @@ class PlatformViewClipRectAfterMovedScenario extends Scenario with _BasePlatform ..pushClipRect(const Rect.fromLTRB(100, 100, 400, 400)); addPlatformView( - _numberOfFrames == 10? 10000:id, + _numberOfFrames == 10? 10000: id, dispatcher: view.platformDispatcher, sceneBuilder: builder, ); @@ -639,7 +638,6 @@ class PlatformViewClipRRectScenario extends PlatformViewScenario { } } - /// Platform view with clip rrect. /// The bounding rect of the rrect is the same as PlatformView and only the corner radii clips the PlatformView. class PlatformViewLargeClipRRectScenario extends PlatformViewScenario { @@ -878,6 +876,187 @@ class PlatformViewClipPathWithTransformScenario extends PlatformViewScenario { } } +/// Two platform views, both have clip rects +class TwoPlatformViewClipRect extends Scenario + with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + TwoPlatformViewClipRect( + super.view, { + required this.firstId, + required this.secondId, + }); + + /// The platform view identifier to use for the first platform view. + final int firstId; + + /// The platform view identifier to use for the second platform view. + final int secondId; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + builder.pushOffset(0, 600); + builder.pushClipRect(const Rect.fromLTRB(100, 100, 400, 400)); + + addPlatformView( + firstId, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + text: 'platform view 1', + ); + + builder.pop(); + builder.pop(); + + // Use a different rect to differentiate from the 1st clip rect. + builder.pushClipRect(const Rect.fromLTRB(100, 100, 300, 300)); + + addPlatformView( + secondId, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + text: 'platform view 2', + ); + + builder.pop(); + final Scene scene = builder.build(); + view.render(scene); + scene.dispose(); + } +} + +/// Two platform views, both have clip rrects +class TwoPlatformViewClipRRect extends Scenario + with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + TwoPlatformViewClipRRect( + super.view, { + required this.firstId, + required this.secondId, + }); + + /// The platform view identifier to use for the first platform view. + final int firstId; + + /// The platform view identifier to use for the second platform view. + final int secondId; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + builder.pushOffset(0, 600); + builder.pushClipRRect( + RRect.fromLTRBAndCorners( + 0, + 0, + 500, + 500, + topLeft: const Radius.circular(15), + topRight: const Radius.circular(50), + bottomLeft: const Radius.circular(50), + ), + ); + + addPlatformView( + firstId, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + text: 'platform view 1', + ); + + builder.pop(); + builder.pop(); + + // Use a different rrect to differentiate from the 1st clip rrect. + builder.pushClipRRect( + RRect.fromLTRBAndCorners( + 0, + 0, + 500, + 500, + topLeft: const Radius.circular(100), + topRight: const Radius.circular(50), + bottomLeft: const Radius.circular(50), + ), + ); + + addPlatformView( + secondId, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + text: 'platform view 2', + ); + + builder.pop(); + final Scene scene = builder.build(); + view.render(scene); + scene.dispose(); + } +} + +/// Two platform views, both have clip path +class TwoPlatformViewClipPath extends Scenario + with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + TwoPlatformViewClipPath( + super.view, { + required this.firstId, + required this.secondId, + }); + + /// The platform view identifier to use for the first platform view. + final int firstId; + + /// The platform view identifier to use for the second platform view. + final int secondId; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + builder.pushOffset(0, 600); + final Path path = Path() + ..moveTo(100, 100) + ..quadraticBezierTo(50, 250, 100, 400) + ..lineTo(350, 400) + ..cubicTo(400, 300, 300, 200, 350, 100) + ..close(); + + builder.pushClipPath(path); + + addPlatformView( + firstId, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + text: 'platform view 1', + ); + + builder.pop(); + builder.pop(); + + // Use a different path to differentiate from the 1st clip path. + final Path path2 = Path() + ..moveTo(100, 100) + ..quadraticBezierTo(100, 150, 100, 400) + ..lineTo(350, 350) + ..cubicTo(400, 300, 300, 200, 350, 200) + ..close(); + + builder.pushClipPath(path2); + + addPlatformView( + secondId, + dispatcher: view.platformDispatcher, + sceneBuilder: builder, + text: 'platform view 2', + ); + + builder.pop(); + final Scene scene = builder.build(); + view.render(scene); + scene.dispose(); + } +} + /// Platform view with transform. class PlatformViewTransformScenario extends PlatformViewScenario { /// Constructs a platform view with transform scenario. @@ -1044,10 +1223,10 @@ class PlatformViewForOverlappingPlatformViewsScenario extends Scenario /// Creates the PlatformViewForOverlappingPlatformViewsScenario. PlatformViewForOverlappingPlatformViewsScenario( - super.view, { - required this.foregroundId, - required this.backgroundId, - }) { + super.view, { + required this.foregroundId, + required this.backgroundId, + }) { _nextFrame = _firstFrame; } @@ -1151,7 +1330,7 @@ class PlatformViewForOverlappingPlatformViewsScenario extends Scenario view.platformDispatcher.sendPlatformMessage( 'flutter/platform_views', message.buffer.asByteData(), - (ByteData? response) {}, + (ByteData? response) {}, ); } } @@ -1360,7 +1539,7 @@ class PlatformViewScrollingUnderWidget extends Scenario super.view, { required int firstPlatformViewId, required int lastPlatformViewId, - }) : _firstPlatformViewId = firstPlatformViewId, + }) : _firstPlatformViewId = firstPlatformViewId, _lastPlatformViewId = lastPlatformViewId; final int _firstPlatformViewId; @@ -1450,7 +1629,6 @@ void addPlatformView( } final String platformViewKey = '$viewType-$id'; - if (_createdPlatformViews.containsKey(platformViewKey)) { addPlatformViewToSceneBuilder( id, @@ -1473,7 +1651,6 @@ void addPlatformView( const int valueString = 7; const int valueUint8List = 8; const int valueMap = 13; - final Uint8List message = Uint8List.fromList([ valueString, ..._encodeString('create'), diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index ff71e609e0cdd..e18f3a8009525 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -53,6 +53,9 @@ Map _scenarios = { 'platform_view_gesture_reject_after_touches_ended': (FlutterView view) => PlatformViewForTouchIOSScenario(view, id: _viewId++, accept: false, rejectUntilTouchesEnded: true), 'platform_view_gesture_accept_with_overlapping_platform_views': (FlutterView view) => PlatformViewForOverlappingPlatformViewsScenario(view, foregroundId: _viewId++, backgroundId: _viewId++), 'platform_view_scrolling_under_widget':(FlutterView view) => PlatformViewScrollingUnderWidget(view, firstPlatformViewId: _viewId++, lastPlatformViewId: _viewId+=16), + 'two_platform_view_clip_rect': (FlutterView view) => TwoPlatformViewClipRect(view, firstId: _viewId++, secondId: _viewId++), + 'two_platform_view_clip_rrect': (FlutterView view) => TwoPlatformViewClipRRect(view, firstId: _viewId++, secondId: _viewId++), + 'two_platform_view_clip_path': (FlutterView view) => TwoPlatformViewClipPath(view, firstId: _viewId++, secondId: _viewId++), 'tap_status_bar': (FlutterView view) => TouchesScenario(view), 'initial_route_reply': (FlutterView view) => InitialRouteReply(view), 'platform_view_with_continuous_texture': (FlutterView view) => PlatformViewWithContinuousTexture(view, id: _viewId++),