Skip to content

Commit

Permalink
Add children to separate _containerView, if needed
Browse files Browse the repository at this point in the history
Summary:
A lot of the style decorators we have (filters, box shadows, borders, etc) use a separate sub-CALayer to accomplish what they want. This becomes an issue if we are clipping the content to the bounds and if these decorators extend beyond the bounds of the view as they are typically unaffected by this clipping. The main things here are `box-shadow` and `outline`. However, this implementation will let us fix some issues w.r.t content rendering under borders. See later diffs for that.

To fix this, if needed, we insert a `_containerView` to contain all of our subviews, and actually apply the clipping to. If this exists, our UIView will only have one subview, this one. But it may have multiple sublayers.

NOTE: This diff does not actually redirect the clipping. It just inserts the subview to test if this breaks anything in and of itself.

Changelog: [Internal]

Reviewed By: lenaic

Differential Revision: D61414649
  • Loading branch information
joevilches authored and facebook-github-bot committed Aug 23, 2024
1 parent 8d3c4fb commit f60dbd4
Showing 1 changed file with 56 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ @implementation RCTViewComponentView {
BOOL _removeClippedSubviews;
NSMutableArray<UIView *> *_reactSubviews;
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
UIView *_containerView;
BOOL _useCustomContainerView;
}

#ifdef RCT_DYNAMIC_FRAMEWORKS
Expand All @@ -54,6 +56,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_props = ViewShadowNode::defaultSharedProps();
_reactSubviews = [NSMutableArray new];
self.multipleTouchEnabled = YES;
_useCustomContainerView = NO;
}
return self;
}
Expand All @@ -72,7 +75,7 @@ - (void)setContentView:(UIView *)contentView
_contentView = contentView;

if (_contentView) {
[self addSubview:_contentView];
[[self getContainerView] addSubview:_contentView];
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}
}
Expand Down Expand Up @@ -129,7 +132,7 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
if (_removeClippedSubviews) {
[_reactSubviews insertObject:childComponentView atIndex:index];
} else {
[self insertSubview:childComponentView atIndex:index];
[[self getContainerView] insertSubview:childComponentView atIndex:index];
}
}

Expand All @@ -139,19 +142,20 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompo
[_reactSubviews removeObjectAtIndex:index];
} else {
RCTAssert(
childComponentView.superview == self,
childComponentView.superview == [self getContainerView],
@"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
self,
childComponentView,
@(index));
RCTAssert(
(self.subviews.count > index) && [self.subviews objectAtIndex:index] == childComponentView,
([self getContainerView].subviews.count > index) &&
[[self getContainerView].subviews objectAtIndex:index] == childComponentView,
@"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
self,
childComponentView,
@(index),
@([self.subviews indexOfObject:childComponentView]),
@([[self.subviews objectAtIndex:index] tag]));
@([[self getContainerView].subviews indexOfObject:childComponentView]),
@([[[self getContainerView].subviews objectAtIndex:index] tag]));
}

[childComponentView removeFromSuperview];
Expand Down Expand Up @@ -181,7 +185,7 @@ - (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIVie
for (UIView *view in _reactSubviews) {
if (CGRectIntersectsRect(clipRect, view.frame)) {
// View is at least partially visible, so remount it if unmounted
[self addSubview:view];
[[self getContainerView] addSubview:view];
// View is visible, update clipped subviews
[view updateClippedSubviewsWithClipRect:clipRect relativeToView:self];
} else if (view.superview) {
Expand Down Expand Up @@ -220,8 +224,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &

if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) {
_removeClippedSubviews = newViewProps.removeClippedSubviews;
if (_removeClippedSubviews && self.subviews.count > 0) {
_reactSubviews = [NSMutableArray arrayWithArray:self.subviews];
if (_removeClippedSubviews && [self getContainerView].subviews.count > 0) {
_reactSubviews = [NSMutableArray arrayWithArray:[self getContainerView].subviews];
}
}

Expand Down Expand Up @@ -516,6 +520,10 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}

if (_containerView) {
_containerView.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
}

if ((_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) &&
layoutMetrics.frame.size != oldLayoutMetrics.frame.size) {
auto newTransform = _props->resolveTransform(layoutMetrics);
Expand All @@ -536,6 +544,7 @@ - (void)setIsJSResponder:(BOOL)isJSResponder
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
_useCustomContainerView = [self styleWouldClipOverflowInk];
if (!_needsInvalidateLayer) {
return;
}
Expand All @@ -561,6 +570,7 @@ - (void)prepareForRecycle
_eventEmitter.reset();
_isJSResponder = NO;
_removeClippedSubviews = NO;
_useCustomContainerView = NO;
_reactSubviews = [NSMutableArray new];
}

Expand Down Expand Up @@ -675,6 +685,41 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
}
}

- (BOOL)styleWouldClipOverflowInk
{
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
BOOL nonZeroBorderWidth = !(borderMetrics.borderWidths.isUniform() && borderMetrics.borderWidths.left == 0);
return _props->getClipsContentToBounds() && (!_props->boxShadow.empty() || nonZeroBorderWidth);
}

// This UIView is the UIView that holds all subviews. It is sometimes not self
// because we want to render "overflow ink" that extends beyond the bounds of
// the view and is not affected by clipping.
- (UIView *)getContainerView
{
if (_useCustomContainerView) {
if (!_containerView) {
_containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
for (UIView *subview in self.subviews) {
[_containerView addSubview:subview];
}
[self addSubview:_containerView];
}

return _containerView;
} else {
if (_containerView) {
for (UIView *subview in _containerView.subviews) {
[self addSubview:subview];
}
[_containerView removeFromSuperview];
_containerView = nil;
}

return self;
}
}

- (void)invalidateLayer
{
CALayer *layer = self.layer;
Expand Down Expand Up @@ -825,7 +870,7 @@ - (void)invalidateLayer
layer.cornerRadius = cornerRadius;
layer.mask = maskLayer;

for (UIView *subview in self.subviews) {
for (UIView *subview in [self getContainerView].subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
RCTCornerInsets cornerInsets = RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
Expand Down Expand Up @@ -993,7 +1038,7 @@ - (NSString *)accessibilityLabel
return label;
}

return RCTRecursiveAccessibilityLabel(self);
return RCTRecursiveAccessibilityLabel([self getContainerView]);
}

- (BOOL)isAccessibilityElement
Expand Down

0 comments on commit f60dbd4

Please sign in to comment.