Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,13 @@ export type ScrollViewPropsIOS = Readonly<{
*/
zoomScale?: ?number,
/**
* This property specifies how the safe area insets are used to modify the
* content area of the scroll view.
* How the safe area insets modify the content area of the scroll view.
*
* Along the scrollable axis, the insets become content insets, so content
* scrolls under the system UI. Across it, every value except `'never'` pads
* the content container by the insets, so content is laid out clear of the
* system UI, such as the rail on iPhone Duo or camera cutout in landscape,
* while the container's background still extends edge to edge.
*
* @default `'never'`
* @platform ios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
endDraggingSensitivityMultiplier: true,
directionalLockEnabled: true,
disableIntervalMomentum: true,
horizontal: true,
indicatorStyle: true,
inverted: true,
keyboardDismissMode: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Class<RCTComponentViewProtocol> RCTInputAccessoryCls(void) __attribute__((used))
Class<RCTComponentViewProtocol> RCTParagraphCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTPullToRefreshViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTSafeAreaViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTScrollContentViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTScrollViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTSwitchCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTTextInputCls(void) __attribute__((used));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{"Paragraph", RCTParagraphCls},
{"PullToRefreshView", RCTPullToRefreshViewCls},
{"SafeAreaView", RCTSafeAreaViewCls},
{"ScrollContentView", RCTScrollContentViewCls},
{"ScrollView", RCTScrollViewCls},
{"Switch", RCTSwitchCls},
{"TextInput", RCTTextInputCls},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) BOOL inverted;

/*
* The content inset before the correction across the scrollable axis: what the props and the content centering
* give. While `padsContentForSafeArea` is set, `contentInset` carries it less whatever UIKit adds across that axis
* for the safe area.
*/
@property (nonatomic, assign) UIEdgeInsets baseContentInset;
@property (nonatomic, assign) BOOL horizontal;

/*
* Whether `ScrollView` pads the content container by the safe area across the scrollable axis, which it does when
* its `contentInsetAdjustmentBehavior` prop is not `never`. UIKit's own `contentInsetAdjustmentBehavior` can differ,
* as react-native-screens sets it natively on the first scroll view of a screen.
*/
@property (nonatomic, assign) BOOL padsContentForSafeArea;

/*
* Makes `setContentOffset:` method no-op when given `block` is executed.
* The block is being executed synchronously.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,69 @@ - (void)centerContentIfNeeded
if (contentSize.height < boundsSize.height) {
top = (boundsSize.height - contentSize.height) * 0.5f;
}
self.contentInset = UIEdgeInsetsMake(top, left, top, left);
self.baseContentInset = UIEdgeInsetsMake(top, left, top, left);
}

- (void)setBaseContentInset:(UIEdgeInsets)baseContentInset
{
_baseContentInset = baseContentInset;
[self applyBaseContentInset];
}

- (void)setHorizontal:(BOOL)horizontal
{
_horizontal = horizontal;
[self applyBaseContentInset];
}

- (void)setPadsContentForSafeArea:(BOOL)padsContentForSafeArea
{
_padsContentForSafeArea = padsContentForSafeArea;
[self applyBaseContentInset];
}

- (void)adjustedContentInsetDidChange
{
[super adjustedContentInsetDidChange];
[self applyBaseContentInset];
}

/*
* Applies `baseContentInset`, less what UIKit adds across the scrollable axis for the safe area while the content is
* padded for it. `ScrollView` lays the content out clear of the system UI rather than scrolling it under; the
* content inset would apply the safe area a second time.
*/
- (void)applyBaseContentInset
{
UIEdgeInsets contentInset = _baseContentInset;
if (_padsContentForSafeArea) {
UIEdgeInsets adjusted = self.adjustedContentInset;
UIEdgeInsets current = self.contentInset;
if (_horizontal) {
contentInset.top -= adjusted.top - current.top;
contentInset.bottom -= adjusted.bottom - current.bottom;
} else {
contentInset.left -= adjusted.left - current.left;
contentInset.right -= adjusted.right - current.right;
}
}
if (UIEdgeInsetsEqualToEdgeInsets(contentInset, self.contentInset)) {
return;
}
// Content pinned to the leading edge across the scrollable axis stays pinned, as UIKit keeps it when it changes the
// adjusted content inset itself.
UIEdgeInsets before = self.adjustedContentInset;
self.contentInset = contentInset;
UIEdgeInsets after = self.adjustedContentInset;
CGPoint contentOffset = self.contentOffset;
if (_horizontal && contentOffset.y <= -before.top) {
contentOffset.y = -after.top;
} else if (!_horizontal && contentOffset.x <= -before.left) {
contentOffset.x = -after.left;
}
if (!CGPointEqualToPoint(contentOffset, self.contentOffset)) {
self.contentOffset = contentOffset;
}
}

- (void)setContentOffset:(CGPoint)contentOffset
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <UIKit/UIKit.h>

#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

/*
* UIView class for the <ScrollContentView> component, the content container of a <ScrollView>.
*/
@interface RCTScrollContentViewComponentView : RCTViewComponentView

/*
* The safe area the content is laid out clear of, as padding on top of the content container's own. The enclosing
* scroll view sets the pair of insets across its scrollable axis, and zero while its `contentInsetAdjustmentBehavior`
* is `never`.
*/
@property (nonatomic, assign) UIEdgeInsets safeAreaPadding;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "RCTScrollContentViewComponentView.h"

#import <React/RCTI18nUtil.h>
#import <React/RCTUtils.h>
#import <react/renderer/components/scrollview/ScrollContentViewComponentDescriptor.h>
#import <react/renderer/components/scrollview/ScrollContentViewState.h>
#import "RCTConversions.h"
#import "RCTFabricComponentsPlugins.h"

using namespace facebook::react;

@implementation RCTScrollContentViewComponentView {
ScrollContentViewShadowNode::ConcreteState::Shared _state;
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ScrollContentViewShadowNode::defaultSharedProps();
}

return self;
}

- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding
{
_safeAreaPadding = safeAreaPadding;
[self _updateStateIfNecessary];
}

- (void)_updateStateIfNecessary
{
if (!_state) {
return;
}

UIEdgeInsets insets = _safeAreaPadding;
insets.left = RCTRoundPixelValue(insets.left);
insets.top = RCTRoundPixelValue(insets.top);
insets.right = RCTRoundPixelValue(insets.right);
insets.bottom = RCTRoundPixelValue(insets.bottom);

auto newPadding = RCTEdgeInsetsFromUIEdgeInsets(insets);
RCTI18nUtil *i18nUtil = [RCTI18nUtil sharedInstance];
auto layoutDirection = i18nUtil.isRTL ? LayoutDirection::RightToLeft : LayoutDirection::LeftToRight;
bool swapLeftAndRightInRTL = i18nUtil.isRTL && i18nUtil.doLeftAndRightSwapInRTL;
auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold.

_state->updateState(
[=](const ScrollContentViewShadowNode::ConcreteState::Data &oldData)
-> ScrollContentViewShadowNode::ConcreteState::SharedData {
auto deltaPadding = newPadding - oldData.safeAreaPadding;

if (std::abs(deltaPadding.left) < threshold && std::abs(deltaPadding.top) < threshold &&
std::abs(deltaPadding.right) < threshold && std::abs(deltaPadding.bottom) < threshold &&
oldData.layoutDirection == layoutDirection && oldData.swapLeftAndRightInRTL == swapLeftAndRightInRTL) {
return nullptr;
}

auto newData = oldData;
newData.safeAreaPadding = newPadding;
newData.layoutDirection = layoutDirection;
newData.swapLeftAndRightInRTL = swapLeftAndRightInRTL;
return std::make_shared<const ScrollContentViewShadowNode::ConcreteState::Data>(newData);
});
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ScrollContentViewComponentDescriptor>();
}

- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<const ScrollContentViewShadowNode::ConcreteState>(state);
}

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
[self _updateStateIfNecessary];
}

- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
_safeAreaPadding = UIEdgeInsetsZero;
}

@end

Class<RCTComponentViewProtocol> RCTScrollContentViewCls(void)
{
return RCTScrollContentViewComponentView.class;
}
Loading
Loading