Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 49 additions & 70 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@

touch_interceptors_[viewId] =
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]);

ChildClippingView* clipping_view =
[[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
[clipping_view addSubview:touch_interceptor];
root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]);

result(nil);
}
Expand Down Expand Up @@ -317,83 +321,60 @@
return clipCount;
}

UIView* FlutterPlatformViewsController::ReconstructClipViewsChain(int number_of_clips,
UIView* platform_view,
UIView* head_clip_view) {
NSInteger indexInFlutterView = -1;
if (head_clip_view.superview) {
// TODO(cyanglaz): potentially cache the index of oldPlatformViewRoot to make this a O(1).
// https://github.com/flutter/flutter/issues/35023
indexInFlutterView = [flutter_view_.get().subviews indexOfObject:head_clip_view];
[head_clip_view removeFromSuperview];
}
UIView* head = platform_view;
int clipIndex = 0;
// Re-use as much existing clip views as needed.
while (head != head_clip_view && clipIndex < number_of_clips) {
head = head.superview;
clipIndex++;
}
// If there were not enough existing clip views, add more.
while (clipIndex < number_of_clips) {
ChildClippingView* clippingView =
[[[ChildClippingView alloc] initWithFrame:flutter_view_.get().bounds] autorelease];
[clippingView addSubview:head];
head = clippingView;
clipIndex++;
}
[head removeFromSuperview];

if (indexInFlutterView > -1) {
// The chain was previously attached; attach it to the same position.
[flutter_view_.get() insertSubview:head atIndex:indexInFlutterView];
}
return head;
}

void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
UIView* embedded_view) {
FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity));
UIView* head = embedded_view;
ResetAnchor(head.layer);
ResetAnchor(embedded_view.layer);
ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;

std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
while (iter != mutators_stack.Top()) {
// The UIKit frame is set based on the logical resolution instead of physical.
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
// resolution. So we need to scale down to match UIKit's logical resolution.
CGFloat screenScale = [UIScreen mainScreen].scale;
CATransform3D finalTransform = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);

// Mask view needs to be full screen because we might draw platform view pixels outside of the
// `ChildClippingView`. Since the mask view's frame will be based on the `clipView`'s coordinate
// system, we need to convert the flutter_view's frame to the clipView's coordinate system. The
// mask view is not displayed on the screen.
CGRect maskViewFrame = [flutter_view_ convertRect:flutter_view_.get().frame toView:clipView];
FlutterClippingMaskView* maskView =
[[[FlutterClippingMaskView alloc] initWithFrame:maskViewFrame] autorelease];
auto iter = mutators_stack.Begin();
while (iter != mutators_stack.End()) {
switch ((*iter)->GetType()) {
case transform: {
CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix());
head.layer.transform = CATransform3DConcat(head.layer.transform, transform);
finalTransform = CATransform3DConcat(transform, finalTransform);
break;
}
case clip_rect:
[maskView clipRect:(*iter)->GetRect() matrix:finalTransform];
break;
case clip_rrect:
case clip_path: {
ChildClippingView* clipView = (ChildClippingView*)head.superview;
clipView.layer.transform = CATransform3DIdentity;
[clipView setClip:(*iter)->GetType()
rect:(*iter)->GetRect()
rrect:(*iter)->GetRRect()
path:(*iter)->GetPath()];
ResetAnchor(clipView.layer);
head = clipView;
[maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform];
break;
case clip_path:
[maskView clipPath:(*iter)->GetPath() matrix:finalTransform];
break;
}
case opacity:
embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
break;
}
++iter;
}
// Reverse scale based on screen scale.
// Reverse the offset of the clipView.
// The clipView's frame includes the final translate of the final transform matrix.
// So we need to revese this translate so the platform view can layout at the correct offset.
//
// The UIKit frame is set based on the logical resolution instead of physical.
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
// resolution. So we need to scale down to match UIKit's logical resolution.
CGFloat screenScale = [UIScreen mainScreen].scale;
head.layer.transform = CATransform3DConcat(
head.layer.transform, CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1));
// Note that we don't apply this transform matrix the clippings because clippings happen on the
// mask view, whose origin is alwasy (0,0) to the flutter_view.
CATransform3D reverseTranslate =
CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0);
embedded_view.layer.transform = CATransform3DConcat(finalTransform, reverseTranslate);
clipView.maskView = maskView;
}

void FlutterPlatformViewsController::CompositeWithParams(int view_id,
Expand All @@ -406,17 +387,15 @@
touchInterceptor.alpha = 1;

const MutatorsStack& mutatorStack = params.mutatorsStack();
int currentClippingCount = CountClips(mutatorStack);
int previousClippingCount = clip_count_[view_id];
if (currentClippingCount != previousClippingCount) {
clip_count_[view_id] = currentClippingCount;
// If we have a different clipping count in this frame, we need to reconstruct the
// ClippingChildView chain to prepare for `ApplyMutators`.
UIView* oldPlatformViewRoot = root_views_[view_id].get();
UIView* newPlatformViewRoot =
ReconstructClipViewsChain(currentClippingCount, touchInterceptor, oldPlatformViewRoot);
root_views_[view_id] = fml::scoped_nsobject<UIView>([newPlatformViewRoot retain]);
}
UIView* clippingView = root_views_[view_id].get();
// The frame of the clipping view should be the final bounding rect.
// Because the translate matrix in the Mutator Stack also includes the offset,
// when we apply the transforms matrix in |ApplyMutators|, we need
// to remember to do a reverse translate.
const SkRect& rect = params.finalBoundingRect();
CGFloat screenScale = [UIScreen mainScreen].scale;
clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
rect.width() / screenScale, rect.height() / screenScale);
ApplyMutators(mutatorStack, touchInterceptor);
}

Expand Down
125 changes: 125 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
FLUTTER_ASSERT_NOT_ARC
@class FlutterPlatformViewsTestMockPlatformView;
static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil;
const float kFloatCompareEpsilon = 0.001;

@interface FlutterPlatformViewsTestMockPlatformView : UIView
@end
Expand Down Expand Up @@ -143,4 +144,128 @@ - (void)testCanCreatePlatformViewWithoutFlutterView {
flutterPlatformViewsController->Reset();
}

- (void)testChildClippingViewHitTests {
ChildClippingView *childCilppingView = [[[ChildClippingView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
UIView *childView = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)] autorelease];
[childCilppingView addSubview:childView];

XCTAssertFalse([childCilppingView pointInside:CGPointMake(50, 50) withEvent:nil]);
XCTAssertTrue([childCilppingView pointInside:CGPointMake(150, 150) withEvent:nil]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might want to check the edges as well. e.g. 100,100, 99,99, 100,99, 99,100.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with the right, bottom edge

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

- (void)testCompositePlatformView {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);

auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();

FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
[[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
flutterPlatformViewsController->RegisterViewFactory(
factory, @"MockFlutterPlatformView",
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
FlutterResult result = ^(id result) {
};
flutterPlatformViewsController->OnMethodCall(
[FlutterMethodCall
methodCallWithMethodName:@"create"
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
result);

XCTAssertNotNil(gMockPlatformView);

UIView *mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
// Create embedded view params
flutter::MutatorsStack stack;
// Layer tree always pushes a screen scale factor to the stack
SkMatrix screenScaleMatrix = SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what [UIScreen mainScreen].scale would be when executed on LUCI. Maybe pick a constant value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to be screenScale because the CompositeEmbeddedView uses screenScale to add a reverse scale matrix to the final transform matrix.

stack.PushTransform(screenScaleMatrix);
// Push a translate matrix
SkMatrix translateMatrix = SkMatrix::MakeTrans(100, 100);
stack.PushTransform(translateMatrix);
SkMatrix finalMatrix;
finalMatrix.setConcat(screenScaleMatrix, translateMatrix);

auto embeddedViewParams = std::make_unique<flutter::EmbeddedViewParams>(finalMatrix, SkSize::Make(300, 300), stack);

flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
flutterPlatformViewsController->CompositeEmbeddedView(2);
CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds
toView:mockFlutterView];
XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300)));
flutterPlatformViewsController->Reset();
}

- (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);

auto flutterPlatformViewsController = std::make_unique<flutter::FlutterPlatformViewsController>();

FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
[[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
flutterPlatformViewsController->RegisterViewFactory(
factory, @"MockFlutterPlatformView",
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
FlutterResult result = ^(id result) {
};
flutterPlatformViewsController->OnMethodCall(
[FlutterMethodCall
methodCallWithMethodName:@"create"
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
result);

XCTAssertNotNil(gMockPlatformView);

UIView *mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
flutterPlatformViewsController->SetFlutterView(mockFlutterView);
// Create embedded view params
flutter::MutatorsStack stack;
// Layer tree always pushes a screen scale factor to the stack
SkMatrix screenScaleMatrix = SkMatrix::MakeScale([UIScreen mainScreen].scale, [UIScreen mainScreen].scale);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

stack.PushTransform(screenScaleMatrix);
// Push a rotate matrix
SkMatrix rotateMatrix;
rotateMatrix.setRotate(10);
stack.PushTransform(rotateMatrix);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but what about PushClipPath?

void PushClipPath(const SkPath& path);

Should that be in a separate unit test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test the clipping, we will probably have to render the view into an image, then check the pixels (sort like a screenshot). I think the scenario tests already covered all the clippings, it is kind of redundant.

SkMatrix finalMatrix;
finalMatrix.setConcat(screenScaleMatrix, rotateMatrix);

auto embeddedViewParams = std::make_unique<flutter::EmbeddedViewParams>(finalMatrix, SkSize::Make(300, 300), stack);

flutterPlatformViewsController->PrerollCompositeEmbeddedView(2, std::move(embeddedViewParams));
flutterPlatformViewsController->CompositeEmbeddedView(2);
CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds
toView:mockFlutterView];
XCTAssertTrue([gMockPlatformView.superview.superview isKindOfClass: ChildClippingView.class]);
ChildClippingView *childClippingView = (ChildClippingView *)gMockPlatformView.superview.superview;
// The childclippingview's frame is set based on flow, but the platform view's frame is set based on quartz.
// Although they should be the same, but we should tolerate small floating point errors.
XCTAssertTrue(fabs(platformViewRectInFlutterView.origin.x - childClippingView.frame.origin.x) < kFloatCompareEpsilon);
XCTAssertTrue(fabs(platformViewRectInFlutterView.origin.y - childClippingView.frame.origin.y) < kFloatCompareEpsilon);
XCTAssertTrue(fabs(platformViewRectInFlutterView.size.width - childClippingView.frame.size.width) < kFloatCompareEpsilon);
XCTAssertTrue(fabs(platformViewRectInFlutterView.size.height - childClippingView.frame.size.height) < kFloatCompareEpsilon);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


flutterPlatformViewsController->Reset();
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@
#include "flutter/shell/platform/darwin/ios/ios_context.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"

// A UIView that acts as a clipping mask for the |ChildClippingView|.
//
// On the [UIView drawRect:] method, this view performs a series of clipping operations and sets the
// alpha channel to the final resulting area to be 1; it also sets the "clipped out" area's alpha
// channel to be 0.
//
// When a UIView sets a |FlutterClippingMaskView| as its `maskView`, the alpha channel of the UIView
// is replaced with the alpha channel of the |FlutterClippingMaskView|.
@interface FlutterClippingMaskView : UIView

// Adds a clip rect operation to the queue.
//
// The `clipSkRect` is transformed with the `matrix` before adding to the queue.
- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix;

// Adds a clip rrect operation to the queue.
//
// The `clipSkRRect` is transformed with the `matrix` before adding to the queue.
- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix;

// Adds a clip path operation to the queue.
//
// The `path` is transformed with the `matrix` before adding to the queue.
- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix;

@end

// A UIView that is used as the parent for embedded UIViews.
//
// This view has 2 roles:
Expand All @@ -37,14 +64,6 @@
// The parent view handles clipping to its subviews.
@interface ChildClippingView : UIView

// Performs the clipping based on the type.
//
// The `type` must be one of the 3: clip_rect, clip_rrect, clip_path.
- (void)setClip:(flutter::MutatorType)type
rect:(const SkRect&)rect
rrect:(const SkRRect&)rrect
path:(const SkPath&)path;

@end

namespace flutter {
Expand Down Expand Up @@ -253,20 +272,6 @@ class FlutterPlatformViewsController {
// Traverse the `mutators_stack` and return the number of clip operations.
int CountClips(const MutatorsStack& mutators_stack);

// Make sure that platform_view has exactly clip_count ChildClippingView ancestors.
//
// Existing ChildClippingViews are re-used. If there are currently more ChildClippingView
// ancestors than needed, the extra views are detached. If there are less ChildClippingView
// ancestors than needed, new ChildClippingViews will be added.
//
// If head_clip_view was attached as a subview to FlutterView, the head of the newly constructed
// ChildClippingViews chain is attached to FlutterView in the same position.
//
// Returns the new head of the clip views chain.
UIView* ReconstructClipViewsChain(int number_of_clips,
UIView* platform_view,
UIView* head_clip_view);

// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
// `ReconstructClipViewsChain`
//
Expand Down
Loading