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 4 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
116 changes: 45 additions & 71 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,55 @@
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;

// 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.
CATransform3D finalTransform =
CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0);
// Reverse scale based on screen scale.
//
// The UIKit frame is set based on the logical resolution instead of physical.
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
// resolution. So we need to scale down to match UIKit's logical resolution.
CGFloat screenScale = [UIScreen mainScreen].scale;
finalTransform = CATransform3DConcat(CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1),
finalTransform);

std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
while (iter != mutators_stack.Top()) {
FlutterClippingMaskView* maskView =
[[[FlutterClippingMaskView alloc] initWithFrame:clipView.bounds] 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.
//
// 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));
embedded_view.layer.transform = finalTransform;
clipView.maskView = maskView;
}

void FlutterPlatformViewsController::CompositeWithParams(int view_id,
Expand All @@ -406,17 +382,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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@
#include "flutter/shell/platform/darwin/ios/ios_context.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"

// A UIView acts as a clipping mask for the |ChildClippingView|.
Comment thread
cyanglaz marked this conversation as resolved.
Outdated
//
// On |DrawRect:|, this view performs a series of clipping operations and sets the alpha channel

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should this interface contain the virtual drawRect method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This View is an OBJC class, drawRect is inherited from the UIView. So I don't think any virtual keyword is necessary here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sg. do you where I can read about the doc format? I'm not sure what |DrawRect:| means, but I can see it means what you described.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I mean the notation. It reads like a class to me, but I’m not sure what the colon symbol is used for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah you are right, let me fix that

// 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 +63,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 +271,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
Original file line number Diff line number Diff line change
Expand Up @@ -53,32 +53,72 @@ void ResetAnchor(CALayer* layer) {

@implementation ChildClippingView

+ (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect {
return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft,
clipSkRect.fBottom - clipSkRect.fTop);
// The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to
// be hit tested and consumed by this view if they are inside the embedded platform view which could
// be smaller the embedded platform view is rotated.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
for (UIView* view in self.subviews) {
if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}

@end

@interface FlutterClippingMaskView ()

- (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect;

@end

@implementation FlutterClippingMaskView {
std::vector<fml::CFRef<CGPathRef>> paths_;
}

- (instancetype)initWithFrame:(CGRect)frame {
if ([super initWithFrame:frame]) {
self.backgroundColor = UIColor.clearColor;
}
return self;
}

- (void)clipRect:(const SkRect&)clipSkRect {
CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect];
fml::CFRef<CGPathRef> pathRef(CGPathCreateWithRect(clipRect, nil));
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
clip.path = pathRef;
self.layer.mask = clip;
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);

// For mask view, only the alpha channel is used.
CGContextSetAlpha(context, 1);

for (size_t i = 0; i < paths_.size(); i++) {
CGContextAddPath(context, paths_.at(i));
CGContextClip(context);
}
CGContextFillRect(context, rect);
CGContextRestoreGState(context);
}

- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix {
CGRect clipRect = [self getCGRectFromSkRect:clipSkRect];
CGPathRef path = CGPathCreateWithRect(clipRect, nil);
paths_.push_back([self getTransformedPath:path matrix:matrix]);
}

- (void)clipRRect:(const SkRRect&)clipSkRRect {
- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix {
CGPathRef pathRef = nullptr;
switch (clipSkRRect.getType()) {
case SkRRect::kEmpty_Type: {
break;
}
case SkRRect::kRect_Type: {
[self clipRect:clipSkRRect.rect()];
[self clipRect:clipSkRRect.rect() matrix:matrix];
return;
}
case SkRRect::kOval_Type:
case SkRRect::kSimple_Type: {
CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRRect.rect()];
CGRect clipRect = [self getCGRectFromSkRect:clipSkRRect.rect()];
pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(),
clipSkRRect.getSimpleRadii().y(), nil);
break;
Expand Down Expand Up @@ -129,23 +169,17 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect {
// TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that
// the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge
// clipping on iOS.
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
clip.path = pathRef;
self.layer.mask = clip;
CGPathRelease(pathRef);
paths_.push_back([self getTransformedPath:pathRef matrix:matrix]);
}

- (void)clipPath:(const SkPath&)path {
- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix {
if (!path.isValid()) {
return;
}
fml::CFRef<CGMutablePathRef> pathRef(CGPathCreateMutable());
if (path.isEmpty()) {
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
clip.path = pathRef;
self.layer.mask = clip;
return;
}
CGMutablePathRef pathRef = CGPathCreateMutable();

// Loop through all verbs and translate them into CGPath
SkPath::Iter iter(path, true);
Expand Down Expand Up @@ -197,42 +231,20 @@ - (void)clipPath:(const SkPath&)path {
}
verb = iter.next(pts);
}

CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
clip.path = pathRef;
self.layer.mask = clip;
paths_.push_back([self getTransformedPath:pathRef matrix:matrix]);
}

- (void)setClip:(flutter::MutatorType)type
rect:(const SkRect&)rect
rrect:(const SkRRect&)rrect
path:(const SkPath&)path {
FML_CHECK(type == flutter::clip_rect || type == flutter::clip_rrect ||
type == flutter::clip_path);
switch (type) {
case flutter::clip_rect:
[self clipRect:rect];
break;
case flutter::clip_rrect:
[self clipRRect:rrect];
break;
case flutter::clip_path:
[self clipPath:path];
break;
default:
break;
}
- (fml::CFRef<CGPathRef>)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix {
CGAffineTransform affine =
CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42);
CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine);
CGPathRelease(path);
return fml::CFRef<CGPathRef>(transformedPath);
}

// The ChildClippingView is as big as the FlutterView, we only want touches to be hit tested and
// consumed by this view if they are inside the smaller child view.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
for (UIView* view in self.subviews) {
if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect {
return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft,
clipSkRect.fBottom - clipSkRect.fTop);
}

@end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

gpus_ReturnNotPermittedKillClient crash on iOS 62403 Can you help us have a look,please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The issue doesn't seem to related to this PR?