Skip to content

Commit

Permalink
Implement View.removeClippedSubviews prop
Browse files Browse the repository at this point in the history
Summary:
Changelog: [internal]

Fabric didn't have prop [removeClippedSubviews](https://reactnative.dev/docs/view#removeclippedsubviews) implemented. This diff adds it. It is

Reviewed By: JoshuaGross

Differential Revision: D29906458

fbshipit-source-id: 5851fa41d7facea9aab73ca131b4a0d23a2411ea
  • Loading branch information
sammy-SC authored and facebook-github-bot committed Jul 27, 2021
1 parent 37dc1d4 commit c5f8c31
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 47 deletions.
6 changes: 6 additions & 0 deletions React/Base/RCTConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ RCT_EXTERN NSString *const RCTUserInterfaceStyleDidChangeNotificationTraitCollec
*/
RCT_EXTERN BOOL RCTExperimentGetPreemptiveViewAllocationDisabled(void);
RCT_EXTERN void RCTExperimentSetPreemptiveViewAllocationDisabled(BOOL value);

/*
* Remove clipped subviews
*/
RCT_EXTERN BOOL RCTGetRemoveClippedSubviewsEnabled(void);
RCT_EXTERN void RCTSetRemoveClippedSubviewsEnabled(BOOL value);
15 changes: 15 additions & 0 deletions React/Base/RCTConstants.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ void RCTExperimentSetPreemptiveViewAllocationDisabled(BOOL value)
{
RCTExperimentPreemptiveViewAllocationDisabled = value;
}

/*
* Remove clipped subviews
*/
static BOOL RCTRemoveClippedSubviewsEnabled = NO;

BOOL RCTGetRemoveClippedSubviewsEnabled(void)
{
return RCTRemoveClippedSubviewsEnabled;
}

void RCTSetRemoveClippedSubviewsEnabled(BOOL value)
{
RCTRemoveClippedSubviewsEnabled = value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ @implementation RCTScrollViewComponentView {

CGPoint _contentOffsetWhenClipped;
NSMutableArray<UIView<RCTComponentViewProtocol> *> *_childComponentViews;
BOOL _subviewClippingEnabled;
}

+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
Expand All @@ -112,6 +113,7 @@ - (instancetype)initWithFrame:(CGRect)frame
((RCTEnhancedScrollView *)_scrollView).overridingDelegate = self;
_isUserTriggeredScrolling = NO;
[self addSubview:_scrollView];
_subviewClippingEnabled = RCTGetRemoveClippedSubviewsEnabled();

_containerView = [[UIView alloc] initWithFrame:CGRectZero];
[_scrollView addSubview:_containerView];
Expand Down Expand Up @@ -324,15 +326,22 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block

- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_childComponentViews insertObject:childComponentView atIndex:index];
if (_subviewClippingEnabled) {
[_containerView insertSubview:childComponentView atIndex:index];
} else {
[_childComponentViews insertObject:childComponentView atIndex:index];
}
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[_childComponentViews objectAtIndex:index] == childComponentView,
@"Attempt to unmount improperly mounted component view.");
[_childComponentViews removeObjectAtIndex:index];
if (!_subviewClippingEnabled) {
RCTAssert(
[_childComponentViews objectAtIndex:index] == childComponentView,
@"Attempt to unmount improperly mounted component view.");
[_childComponentViews removeObjectAtIndex:index];
}

// In addition to removing a view from `_childComponentViews`,
// we have to unmount views immediately to not mess with recycling.
[childComponentView removeFromSuperview];
Expand Down Expand Up @@ -609,64 +618,68 @@ - (void)_remountChildrenIfNeeded

- (void)_remountChildren
{
CGRect visibleFrame = [_scrollView convertRect:_scrollView.bounds toView:_containerView];
visibleFrame = CGRectInset(visibleFrame, -kClippingLeeway, -kClippingLeeway);

// `zoomScale` is negative in RTL. Absolute value is needed.
CGFloat scale = 1.0 / std::abs(_scrollView.zoomScale);
visibleFrame.origin.x *= scale;
visibleFrame.origin.y *= scale;
visibleFrame.size.width *= scale;
visibleFrame.size.height *= scale;

if (_subviewClippingEnabled) {
[_scrollView updateClippedSubviewsWithClipRect:CGRectInset(_scrollView.bounds, -kClippingLeeway, -kClippingLeeway)
relativeToView:_scrollView];
} else {
CGRect visibleFrame = [_scrollView convertRect:_scrollView.bounds toView:_containerView];
visibleFrame = CGRectInset(visibleFrame, -kClippingLeeway, -kClippingLeeway);

// `zoomScale` is negative in RTL. Absolute value is needed.
CGFloat scale = 1.0 / std::abs(_scrollView.zoomScale);
visibleFrame.origin.x *= scale;
visibleFrame.origin.y *= scale;
visibleFrame.size.width *= scale;
visibleFrame.size.height *= scale;
#ifndef NDEBUG
NSMutableArray<UIView<RCTComponentViewProtocol> *> *expectedSubviews = [NSMutableArray new];
NSMutableArray<UIView<RCTComponentViewProtocol> *> *expectedSubviews = [NSMutableArray new];
#endif

NSInteger mountedIndex = 0;
for (UIView *componentView in _childComponentViews) {
BOOL shouldBeMounted = YES;
BOOL isMounted = componentView.superview != nil;
NSInteger mountedIndex = 0;
for (UIView *componentView in _childComponentViews) {
BOOL shouldBeMounted = YES;
BOOL isMounted = componentView.superview != nil;

// It's simpler and faster to not mess with views that are not `RCTViewComponentView` subclasses.
if ([componentView isKindOfClass:[RCTViewComponentView class]]) {
RCTViewComponentView *viewComponentView = (RCTViewComponentView *)componentView;
auto layoutMetrics = viewComponentView->_layoutMetrics;
// It's simpler and faster to not mess with views that are not `RCTViewComponentView` subclasses.
if ([componentView isKindOfClass:[RCTViewComponentView class]]) {
RCTViewComponentView *viewComponentView = (RCTViewComponentView *)componentView;
auto layoutMetrics = viewComponentView->_layoutMetrics;

if (layoutMetrics.overflowInset == EdgeInsets{}) {
shouldBeMounted = CGRectIntersectsRect(visibleFrame, componentView.frame);
if (layoutMetrics.overflowInset == EdgeInsets{}) {
shouldBeMounted = CGRectIntersectsRect(visibleFrame, componentView.frame);
}
}
}

if (shouldBeMounted != isMounted) {
if (shouldBeMounted) {
[_containerView insertSubview:componentView atIndex:mountedIndex];
} else {
[componentView removeFromSuperview];
if (shouldBeMounted != isMounted) {
if (shouldBeMounted) {
[_containerView insertSubview:componentView atIndex:mountedIndex];
} else {
[componentView removeFromSuperview];
}
}
}

if (shouldBeMounted) {
mountedIndex++;
}
if (shouldBeMounted) {
mountedIndex++;
}

#ifndef NDEBUG
if (shouldBeMounted) {
[expectedSubviews addObject:componentView];
}
if (shouldBeMounted) {
[expectedSubviews addObject:componentView];
}
#endif
}
}

#ifndef NDEBUG
RCTAssert(
_containerView.subviews.count == expectedSubviews.count,
@"-[RCTScrollViewComponentView _remountChildren]: Inconsistency detected.");
for (NSInteger i = 0; i < expectedSubviews.count; i++) {
RCTAssert(
[_containerView.subviews objectAtIndex:i] == [expectedSubviews objectAtIndex:i],
_containerView.subviews.count == expectedSubviews.count,
@"-[RCTScrollViewComponentView _remountChildren]: Inconsistency detected.");
}
for (NSInteger i = 0; i < expectedSubviews.count; i++) {
RCTAssert(
[_containerView.subviews objectAtIndex:i] == [expectedSubviews objectAtIndex:i],
@"-[RCTScrollViewComponentView _remountChildren]: Inconsistency detected.");
}
#endif
}
}

#pragma mark - RCTScrollableProtocol
Expand Down
88 changes: 88 additions & 0 deletions React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ @implementation RCTViewComponentView {
CALayer *_borderLayer;
BOOL _needsInvalidateLayer;
BOOL _isJSResponder;
BOOL _removeClippedSubviews;
NSMutableArray<UIView *> *_reactSubviews;
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
}

Expand All @@ -32,6 +34,7 @@ - (instancetype)initWithFrame:(CGRect)frame
if (self = [super initWithFrame:frame]) {
static auto const defaultProps = std::make_shared<ViewProps const>();
_props = defaultProps;
_reactSubviews = [NSMutableArray new];
self.multipleTouchEnabled = YES;
}
return self;
Expand Down Expand Up @@ -86,6 +89,81 @@ + (ComponentDescriptorProvider)componentDescriptorProvider
return concreteComponentDescriptorProvider<ViewComponentDescriptor>();
}

- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
childComponentView.superview == nil,
@"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));

if (_removeClippedSubviews) {
[_reactSubviews insertObject:childComponentView atIndex:index];
} else {
[self insertSubview:childComponentView atIndex:index];
}
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (_removeClippedSubviews) {
[_reactSubviews removeObjectAtIndex:index];
} else {
RCTAssert(
childComponentView.superview == self,
@"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,
@"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]));
}

[childComponentView removeFromSuperview];
}

- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
if (!_removeClippedSubviews) {
// Use default behavior if unmounting is disabled
return [super updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}

if (_reactSubviews.count == 0) {
// Do nothing if we have no subviews
return;
}

if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
// Do nothing if layout hasn't happened yet
return;
}

// Convert clipping rect to local coordinates
clipRect = [clipView convertRect:clipRect toView:self];

// Mount / unmount views
for (UIView *view in _reactSubviews) {
if (CGRectIntersectsRect(clipRect, view.frame)) {
// View is at least partially visible, so remount it if unmounted
[self addSubview:view];
// View is visible, update clipped subviews
[view updateClippedSubviewsWithClipRect:clipRect relativeToView:self];
} else if (view.superview) {
// View is completely outside the clipRect, so unmount it
[view removeFromSuperview];
}
}
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
RCTAssert(props, @"`props` must not be `null`.");
Expand Down Expand Up @@ -113,6 +191,14 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
needsInvalidateLayer = YES;
}

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

// `backgroundColor`
if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
Expand Down Expand Up @@ -322,6 +408,8 @@ - (void)prepareForRecycle
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil;
_eventEmitter.reset();
_isJSResponder = NO;
_removeClippedSubviews = NO;
_reactSubviews = [NSMutableArray new];
}

- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props
Expand Down
2 changes: 2 additions & 0 deletions React/Fabric/Mounting/UIView+ComponentViewProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(nullable NSSet<NSString *> *)props;
- (nullable NSSet<NSString *> *)propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;

- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;

@end

NS_ASSUME_NONNULL_END
12 changes: 12 additions & 0 deletions React/Fabric/Mounting/UIView+ComponentViewProtocol.mm
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,16 @@ - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(nullable NSSet<N
return nil;
}

- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
clipRect = [clipView convertRect:clipRect toView:self];

// Normal views don't support unmounting, so all
// this does is forward message to our subviews,
// in case any of those do support it
for (UIView *subview in self.subviews) {
[subview updateClippedSubviewsWithClipRect:clipRect relativeToView:self];
}
}

@end
4 changes: 4 additions & 0 deletions React/Fabric/RCTSurfacePresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ - (RCTScheduler *)_createScheduler
RCTExperimentSetPreemptiveViewAllocationDisabled(YES);
}

if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_remove_clipped_subviews_ios")) {
RCTSetRemoveClippedSubviewsEnabled(YES);
}

auto componentRegistryFactory =
[factory = wrapManagedObject(_mountingManager.componentViewRegistry.componentViewFactory)](
EventDispatcher::Weak const &eventDispatcher, ContextContainer::Shared const &contextContainer) {
Expand Down
5 changes: 5 additions & 0 deletions ReactCommon/react/renderer/components/view/ViewProps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ ViewProps::ViewProps(ViewProps const &sourceProps, RawProps const &rawProps)
"collapsable",
sourceProps.collapsable,
true)),
removeClippedSubviews(convertRawProp(
rawProps,
"removeClippedSubviews",
sourceProps.removeClippedSubviews,
false)),
elevation(
convertRawProp(rawProps, "elevation", sourceProps.elevation, {})){};

Expand Down
2 changes: 2 additions & 0 deletions ReactCommon/react/renderer/components/view/ViewProps.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps {

bool collapsable{true};

bool removeClippedSubviews{false};

Float elevation{}; /* Android-only */

#pragma mark - Convenience Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ void ViewShadowNode::initialize() noexcept {
viewProps.getClipsContentToBounds() ||
isColorMeaningful(viewProps.shadowColor) ||
viewProps.accessibilityElementsHidden ||
viewProps.importantForAccessibility != ImportantForAccessibility::Auto;
viewProps.importantForAccessibility != ImportantForAccessibility::Auto ||
viewProps.removeClippedSubviews;

bool formsView = isColorMeaningful(viewProps.backgroundColor) ||
isColorMeaningful(viewProps.foregroundColor) ||
Expand Down

0 comments on commit c5f8c31

Please sign in to comment.