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

Commit 02d71d6

Browse files
author
Chris Yang
authored
Use a single mask view to clip iOS platform view (#20050)
1 parent ea811fc commit 02d71d6

File tree

6 files changed

+523
-145
lines changed

6 files changed

+523
-145
lines changed

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

Lines changed: 49 additions & 70 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,60 @@
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;
359329

360-
std::vector<std::shared_ptr<Mutator>>::const_reverse_iterator iter = mutators_stack.Bottom();
361-
while (iter != mutators_stack.Top()) {
330+
// The UIKit frame is set based on the logical resolution instead of physical.
331+
// (https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html).
332+
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
333+
// 500 points in UIKit. And until this point, we did all the calculation based on the flow
334+
// resolution. So we need to scale down to match UIKit's logical resolution.
335+
CGFloat screenScale = [UIScreen mainScreen].scale;
336+
CATransform3D finalTransform = CATransform3DMakeScale(1 / screenScale, 1 / screenScale, 1);
337+
338+
// Mask view needs to be full screen because we might draw platform view pixels outside of the
339+
// `ChildClippingView`. Since the mask view's frame will be based on the `clipView`'s coordinate
340+
// system, we need to convert the flutter_view's frame to the clipView's coordinate system. The
341+
// mask view is not displayed on the screen.
342+
CGRect maskViewFrame = [flutter_view_ convertRect:flutter_view_.get().frame toView:clipView];
343+
FlutterClippingMaskView* maskView =
344+
[[[FlutterClippingMaskView alloc] initWithFrame:maskViewFrame] autorelease];
345+
auto iter = mutators_stack.Begin();
346+
while (iter != mutators_stack.End()) {
362347
switch ((*iter)->GetType()) {
363348
case transform: {
364349
CATransform3D transform = GetCATransform3DFromSkMatrix((*iter)->GetMatrix());
365-
head.layer.transform = CATransform3DConcat(head.layer.transform, transform);
350+
finalTransform = CATransform3DConcat(transform, finalTransform);
366351
break;
367352
}
368353
case clip_rect:
354+
[maskView clipRect:(*iter)->GetRect() matrix:finalTransform];
355+
break;
369356
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;
357+
[maskView clipRRect:(*iter)->GetRRect() matrix:finalTransform];
358+
break;
359+
case clip_path:
360+
[maskView clipPath:(*iter)->GetPath() matrix:finalTransform];
379361
break;
380-
}
381362
case opacity:
382363
embedded_view.alpha = (*iter)->GetAlphaFloat() * embedded_view.alpha;
383364
break;
384365
}
385366
++iter;
386367
}
387-
// Reverse scale based on screen scale.
368+
// Reverse the offset of the clipView.
369+
// The clipView's frame includes the final translate of the final transform matrix.
370+
// So we need to revese this translate so the platform view can layout at the correct offset.
388371
//
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));
372+
// Note that we don't apply this transform matrix the clippings because clippings happen on the
373+
// mask view, whose origin is alwasy (0,0) to the flutter_view.
374+
CATransform3D reverseTranslate =
375+
CATransform3DMakeTranslation(-clipView.frame.origin.x, -clipView.frame.origin.y, 0);
376+
embedded_view.layer.transform = CATransform3DConcat(finalTransform, reverseTranslate);
377+
clipView.maskView = maskView;
397378
}
398379

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

408389
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-
}
390+
UIView* clippingView = root_views_[view_id].get();
391+
// The frame of the clipping view should be the final bounding rect.
392+
// Because the translate matrix in the Mutator Stack also includes the offset,
393+
// when we apply the transforms matrix in |ApplyMutators|, we need
394+
// to remember to do a reverse translate.
395+
const SkRect& rect = params.finalBoundingRect();
396+
CGFloat screenScale = [UIScreen mainScreen].scale;
397+
clippingView.frame = CGRectMake(rect.x() / screenScale, rect.y() / screenScale,
398+
rect.width() / screenScale, rect.height() / screenScale);
420399
ApplyMutators(mutatorStack, touchInterceptor);
421400
}
422401

0 commit comments

Comments
 (0)