Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit db33eae

Browse files
author
Chris Yang
committed
use mask view to clip
1 parent c0ac43d commit db33eae

File tree

3 files changed

+143
-144
lines changed

3 files changed

+143
-144
lines changed

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm

Lines changed: 45 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,11 @@
167167

168168
touch_interceptors_[viewId] =
169169
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
170-
root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]);
170+
171+
ChildClippingView* clipping_view =
172+
[[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
173+
[clipping_view addSubview:touch_interceptor];
174+
root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]);
171175

172176
result(nil);
173177
}
@@ -317,83 +321,55 @@
317321
return clipCount;
318322
}
319323

320-
UIView* FlutterPlatformViewsController::ReconstructClipViewsChain(int number_of_clips,
321-
UIView* platform_view,
322-
UIView* head_clip_view) {
323-
NSInteger indexInFlutterView = -1;
324-
if (head_clip_view.superview) {
325-
// TODO(cyanglaz): potentially cache the index of oldPlatformViewRoot to make this a O(1).
326-
// https://github.com/flutter/flutter/issues/35023
327-
indexInFlutterView = [flutter_view_.get().subviews indexOfObject:head_clip_view];
328-
[head_clip_view removeFromSuperview];
329-
}
330-
UIView* head = platform_view;
331-
int clipIndex = 0;
332-
// Re-use as much existing clip views as needed.
333-
while (head != head_clip_view && clipIndex < number_of_clips) {
334-
head = head.superview;
335-
clipIndex++;
336-
}
337-
// If there were not enough existing clip views, add more.
338-
while (clipIndex < number_of_clips) {
339-
ChildClippingView* clippingView =
340-
[[[ChildClippingView alloc] initWithFrame:flutter_view_.get().bounds] autorelease];
341-
[clippingView addSubview:head];
342-
head = clippingView;
343-
clipIndex++;
344-
}
345-
[head removeFromSuperview];
346-
347-
if (indexInFlutterView > -1) {
348-
// The chain was previously attached; attach it to the same position.
349-
[flutter_view_.get() insertSubview:head atIndex:indexInFlutterView];
350-
}
351-
return head;
352-
}
353-
354324
void FlutterPlatformViewsController::ApplyMutators(const MutatorsStack& mutators_stack,
355325
UIView* embedded_view) {
356326
FML_DCHECK(CATransform3DEqualToTransform(embedded_view.layer.transform, CATransform3DIdentity));
357-
UIView* head = embedded_view;
358-
ResetAnchor(head.layer);
327+
ResetAnchor(embedded_view.layer);
328+
ChildClippingView* clipView = (ChildClippingView*)embedded_view.superview;
329+
330+
// Reverse the offset of the clipView.
331+
// The clipView's frame includes the final translate of the final transform matrix.
332+
// So we need to revese this translate so the platform view can layout at the correct offset.
333+
CATransform3D finalTransform =
334+
CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0);
335+
// Reverse scale based on screen scale.
336+
//
337+
// The UIKit frame is set based on the logical resolution instead of physical.
338+
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
339+
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
340+
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
341+
// resolution. So we need to scale down to match UIKit's logical resolution.
342+
CGFloat screenScale = [UIScreen mainScreen].scale;
343+
finalTransform = CATransform3DConcat(CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1),
344+
finalTransform);
359345

360-
std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
361-
while (iter != mutators_stack.Top()) {
346+
FlutterClippingMaskView* maskView =
347+
[[[FlutterClippingMaskView alloc] initWithFrame:clipView.bounds] autorelease];
348+
auto iter = mutators_stack.Begin();
349+
while (iter != mutators_stack.End()) {
362350
switch ((*iter)->GetType()) {
363351
case transform: {
364352
CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix());
365-
head.layer.transform = CATransform3DConcat(head.layer.transform, transform);
353+
finalTransform = CATransform3DConcat(transform, finalTransform);
366354
break;
367355
}
368356
case clip_rect:
357+
[maskView clipRect:(*iter)->GetRect() matrix:finalTransform];
358+
break;
369359
case clip_rrect:
370-
case clip_path: {
371-
ChildClippingView* clipView = (ChildClippingView*)head.superview;
372-
clipView.layer.transform = CATransform3DIdentity;
373-
[clipView setClip:(*iter)->GetType()
374-
rect:(*iter)->GetRect()
375-
rrect:(*iter)->GetRRect()
376-
path:(*iter)->GetPath()];
377-
ResetAnchor(clipView.layer);
378-
head = clipView;
360+
[maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform];
361+
break;
362+
case clip_path:
363+
[maskView clipPath:(*iter)->GetPath() matrix:finalTransform];
379364
break;
380-
}
381365
case opacity:
382366
embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
383367
break;
384368
}
385369
++iter;
386370
}
387-
// Reverse scale based on screen scale.
388-
//
389-
// The UIKit frame is set based on the logical resolution instead of physical.
390-
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
391-
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
392-
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
393-
// resolution. So we need to scale down to match UIKit's logical resolution.
394-
CGFloat screenScale = [UIScreen mainScreen].scale;
395-
head.layer.transform = CATransform3DConcat(
396-
head.layer.transform, CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1));
371+
embedded_view.layer.transform = finalTransform;
372+
clipView.maskView = maskView;
397373
}
398374

399375
void FlutterPlatformViewsController::CompositeWithParams(int view_id,
@@ -406,17 +382,15 @@
406382
touchInterceptor.alpha = 1;
407383

408384
const MutatorsStack& mutatorStack = params.mutatorsStack();
409-
int currentClippingCount = CountClips(mutatorStack);
410-
int previousClippingCount = clip_count_[view_id];
411-
if (currentClippingCount != previousClippingCount) {
412-
clip_count_[view_id] = currentClippingCount;
413-
// If we have a different clipping count in this frame, we need to reconstruct the
414-
// ClippingChildView chain to prepare for `ApplyMutators`.
415-
UIView* oldPlatformViewRoot = root_views_[view_id].get();
416-
UIView* newPlatformViewRoot =
417-
ReconstructClipViewsChain(currentClippingCount, touchInterceptor, oldPlatformViewRoot);
418-
root_views_[view_id] = fml::scoped_nsobject<UIView>([newPlatformViewRoot retain]);
419-
}
385+
UIView* clippingView = root_views_[view_id].get();
386+
// The frame of the clipping view should be the final bounding rect.
387+
// Because the translate matrix in the Mutator Stack also includes the offset,
388+
// when we apply the transforms matrix in |ApplyMutators|, we need
389+
// to remember to do a reverse translate.
390+
const SkRect& rect = params.finalBoundingRect();
391+
CGFloat screenScale = [UIScreen mainScreen].scale;
392+
clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
393+
rect.width() / screenScale, rect.height() / screenScale);
420394
ApplyMutators(mutatorStack, touchInterceptor);
421395
}
422396

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@
1616
#include "flutter/shell/platform/darwin/ios/ios_context.h"
1717
#include "third_party/skia/include/core/SkPictureRecorder.h"
1818

19+
// A UIView acts as a clipping mask for the |ChildClippingView|.
20+
//
21+
// On |DrawRect:|, this view performs a series of clipping operations and sets the alpha channel
22+
// to the final resulting area to be 1; it also sets the "clipped out" area's alpha channel to be 0.
23+
//
24+
// When a UIView sets a |FlutterClippingMaskView| as its `maskView`, the alpha channel of the UIView
25+
// is replaced with the alpha channel of the |FlutterClippingMaskView|.
26+
@interface FlutterClippingMaskView : UIView
27+
28+
// Adds a clip rect operation to the queue.
29+
//
30+
// The `clipSkRect` is transformed with the `matrix` before adding to the queue.
31+
- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix;
32+
33+
// Adds a clip rrect operation to the queue.
34+
//
35+
// The `clipSkRRect` is transformed with the `matrix` before adding to the queue.
36+
- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix;
37+
38+
// Adds a clip path operation to the queue.
39+
//
40+
// The `path` is transformed with the `matrix` before adding to the queue.
41+
- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix;
42+
43+
@end
44+
1945
// A UIView that is used as the parent for embedded UIViews.
2046
//
2147
// This view has 2 roles:
@@ -37,14 +63,6 @@
3763
// The parent view handles clipping to its subviews.
3864
@interface ChildClippingView : UIView
3965

40-
// Performs the clipping based on the type.
41-
//
42-
// The `type` must be one of the 3: clip_rect, clip_rrect, clip_path.
43-
- (void)setClip:(flutter::MutatorType)type
44-
rect:(const SkRect&)rect
45-
rrect:(const SkRRect&)rrect
46-
path:(const SkPath&)path;
47-
4866
@end
4967

5068
namespace flutter {
@@ -253,20 +271,6 @@ class FlutterPlatformViewsController {
253271
// Traverse the `mutators_stack` and return the number of clip operations.
254272
int CountClips(const MutatorsStack& mutators_stack);
255273

256-
// Make sure that platform_view has exactly clip_count ChildClippingView ancestors.
257-
//
258-
// Existing ChildClippingViews are re-used. If there are currently more ChildClippingView
259-
// ancestors than needed, the extra views are detached. If there are less ChildClippingView
260-
// ancestors than needed, new ChildClippingViews will be added.
261-
//
262-
// If head_clip_view was attached as a subview to FlutterView, the head of the newly constructed
263-
// ChildClippingViews chain is attached to FlutterView in the same position.
264-
//
265-
// Returns the new head of the clip views chain.
266-
UIView* ReconstructClipViewsChain(int number_of_clips,
267-
UIView* platform_view,
268-
UIView* head_clip_view);
269-
270274
// Applies the mutators in the mutators_stack to the UIView chain that was constructed by
271275
// `ReconstructClipViewsChain`
272276
//

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.mm

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -53,32 +53,73 @@ void ResetAnchor(CALayer* layer) {
5353

5454
@implementation ChildClippingView
5555

56-
+ (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect {
57-
return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft,
58-
clipSkRect.fBottom - clipSkRect.fTop);
56+
// The ChildClippingView's frame is the bounding rect of the platform view. we only want touches to
57+
// be hit tested and consumed by this view if they are inside the embedded platform view which could
58+
// be smaller the embedded platform view is rotated.
59+
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
60+
for (UIView* view in self.subviews) {
61+
if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
62+
return YES;
63+
}
64+
}
65+
return NO;
66+
}
67+
68+
@end
69+
70+
@interface FlutterClippingMaskView ()
71+
72+
- (CGPathRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix;
73+
- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect;
74+
75+
@end
76+
77+
@implementation FlutterClippingMaskView {
78+
std::vector<CGPathRef> paths_;
79+
}
80+
81+
- (instancetype)initWithFrame:(CGRect)frame {
82+
if ([super initWithFrame:frame]) {
83+
self.backgroundColor = UIColor.clearColor;
84+
}
85+
return self;
86+
}
87+
88+
- (void)drawRect:(CGRect)rect {
89+
CGContextRef context = UIGraphicsGetCurrentContext();
90+
// For mask view, only the alpha channel is used, the actual color doesn't matter.
91+
// Set to red for easier future debugging.
92+
[UIColor.redColor setFill];
93+
CGContextSaveGState(context);
94+
95+
for (size_t i = 0; i < paths_.size(); i++) {
96+
CGPathRef pathRef = paths_.at(i);
97+
CGContextAddPath(context, pathRef);
98+
CGContextClip(context);
99+
}
100+
CGContextFillRect(context, rect);
101+
CGContextRestoreGState(context);
59102
}
60103

61-
- (void)clipRect:(const SkRect&)clipSkRect {
62-
CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRect];
63-
fml::CFRef<CGPathRef> pathRef(CGPathCreateWithRect(clipRect, nil));
64-
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
65-
clip.path = pathRef;
66-
self.layer.mask = clip;
104+
- (void)clipRect:(const SkRect&)clipSkRect matrix:(const CATransform3D&)matrix {
105+
CGRect clipRect = [self getCGRectFromSkRect:clipSkRect];
106+
CGPathRef path = CGPathCreateWithRect(clipRect, nil);
107+
paths_.push_back([self getTransformedPath:path matrix:matrix]);
67108
}
68109

69-
- (void)clipRRect:(const SkRRect&)clipSkRRect {
110+
- (void)clipRRect:(const SkRRect&)clipSkRRect matrix:(const CATransform3D&)matrix {
70111
CGPathRef pathRef = nullptr;
71112
switch (clipSkRRect.getType()) {
72113
case SkRRect::kEmpty_Type: {
73114
break;
74115
}
75116
case SkRRect::kRect_Type: {
76-
[self clipRect:clipSkRRect.rect()];
117+
[self clipRect:clipSkRRect.rect() matrix:matrix];
77118
return;
78119
}
79120
case SkRRect::kOval_Type:
80121
case SkRRect::kSimple_Type: {
81-
CGRect clipRect = [ChildClippingView getCGRectFromSkRect:clipSkRRect.rect()];
122+
CGRect clipRect = [self getCGRectFromSkRect:clipSkRRect.rect()];
82123
pathRef = CGPathCreateWithRoundedRect(clipRect, clipSkRRect.getSimpleRadii().x(),
83124
clipSkRRect.getSimpleRadii().y(), nil);
84125
break;
@@ -129,23 +170,17 @@ - (void)clipRRect:(const SkRRect&)clipSkRRect {
129170
// TODO(cyanglaz): iOS does not seem to support hard edge on CAShapeLayer. It clearly stated that
130171
// the CAShaperLayer will be drawn antialiased. Need to figure out a way to do the hard edge
131172
// clipping on iOS.
132-
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
133-
clip.path = pathRef;
134-
self.layer.mask = clip;
135-
CGPathRelease(pathRef);
173+
paths_.push_back([self getTransformedPath:pathRef matrix:matrix]);
136174
}
137175

138-
- (void)clipPath:(const SkPath&)path {
176+
- (void)clipPath:(const SkPath&)path matrix:(const CATransform3D&)matrix {
139177
if (!path.isValid()) {
140178
return;
141179
}
142-
fml::CFRef<CGMutablePathRef> pathRef(CGPathCreateMutable());
143180
if (path.isEmpty()) {
144-
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
145-
clip.path = pathRef;
146-
self.layer.mask = clip;
147181
return;
148182
}
183+
CGMutablePathRef pathRef = CGPathCreateMutable();
149184

150185
// Loop through all verbs and translate them into CGPath
151186
SkPath::Iter iter(path, true);
@@ -197,42 +232,28 @@ - (void)clipPath:(const SkPath&)path {
197232
}
198233
verb = iter.next(pts);
199234
}
235+
paths_.push_back([self getTransformedPath:pathRef matrix:matrix]);
236+
}
200237

201-
CAShapeLayer* clip = [[[CAShapeLayer alloc] init] autorelease];
202-
clip.path = pathRef;
203-
self.layer.mask = clip;
238+
- (CGPathRef)getTransformedPath:(CGPathRef)path matrix:(CATransform3D)matrix {
239+
CGAffineTransform affine =
240+
CGAffineTransformMake(matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42);
241+
CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &affine);
242+
CGPathRelease(path);
243+
return transformedPath;
204244
}
205245

206-
- (void)setClip:(flutter::MutatorType)type
207-
rect:(const SkRect&)rect
208-
rrect:(const SkRRect&)rrect
209-
path:(const SkPath&)path {
210-
FML_CHECK(type == flutter::clip_rect || type == flutter::clip_rrect ||
211-
type == flutter::clip_path);
212-
switch (type) {
213-
case flutter::clip_rect:
214-
[self clipRect:rect];
215-
break;
216-
case flutter::clip_rrect:
217-
[self clipRRect:rrect];
218-
break;
219-
case flutter::clip_path:
220-
[self clipPath:path];
221-
break;
222-
default:
223-
break;
224-
}
246+
- (CGRect)getCGRectFromSkRect:(const SkRect&)clipSkRect {
247+
return CGRectMake(clipSkRect.fLeft, clipSkRect.fTop, clipSkRect.fRight - clipSkRect.fLeft,
248+
clipSkRect.fBottom - clipSkRect.fTop);
225249
}
226250

227-
// The ChildClippingView is as big as the FlutterView, we only want touches to be hit tested and
228-
// consumed by this view if they are inside the smaller child view.
229-
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
230-
for (UIView* view in self.subviews) {
231-
if ([view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
232-
return YES;
233-
}
251+
- (void)dealloc {
252+
for (CGPathRef pathRef : paths_) {
253+
CGPathRelease(pathRef);
234254
}
235-
return NO;
255+
paths_.clear();
256+
[super dealloc];
236257
}
237258

238259
@end

0 commit comments

Comments
 (0)