Skip to content
Open
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 @@ -59,6 +59,19 @@ 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 JavaScript pads the content container by the safe area across the scrollable axis. Defaults to NO.
*/
@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,80 @@ - (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;
if (_padsContentForSafeArea) {
[self applyBaseContentInset];
} else {
self.contentInset = baseContentInset;
}
}

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

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

- (void)adjustedContentInsetDidChange
{
[super adjustedContentInsetDidChange];
if (_padsContentForSafeArea) {
[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
Expand Up @@ -204,7 +204,9 @@ - (void)_keyboardWillChangeFrame:(NSNotification *)notification
CGPoint absoluteViewOrigin = [self convertPoint:self.bounds.origin toView:nil];
CGFloat scrollViewLowerY = isInverted ? absoluteViewOrigin.y : absoluteViewOrigin.y + self.bounds.size.height;

UIEdgeInsets newEdgeInsets = _scrollView.contentInset;
auto *enhancedScrollView = (RCTEnhancedScrollView *)_scrollView;
UIEdgeInsets newEdgeInsets =
enhancedScrollView.padsContentForSafeArea ? enhancedScrollView.baseContentInset : enhancedScrollView.contentInset;
CGFloat inset = MAX(scrollViewLowerY - keyboardEndFrame.origin.y, 0);
const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (isInverted) {
Expand Down Expand Up @@ -257,7 +259,7 @@ - (void)_keyboardWillChangeFrame:(NSNotification *)notification
delay:0.0
options:animationOptionsWithCurve(curve)
animations:^{
self->_scrollView.contentInset = newEdgeInsets;
((RCTEnhancedScrollView *)self->_scrollView).baseContentInset = newEdgeInsets;
self->_scrollView.verticalScrollIndicatorInsets = newEdgeInsets;
[self scrollTo:newContentOffset.x y:newContentOffset.y animated:NO];
}
Expand Down Expand Up @@ -345,6 +347,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
MAP_SCROLL_VIEW_PROP(bouncesZoom);
MAP_SCROLL_VIEW_PROP(canCancelContentTouches);
MAP_SCROLL_VIEW_PROP(centerContent);
MAP_SCROLL_VIEW_PROP(horizontal);
// MAP_SCROLL_VIEW_PROP(automaticallyAdjustContentInsets);
MAP_SCROLL_VIEW_PROP(decelerationRate);
MAP_SCROLL_VIEW_PROP(directionalLockEnabled);
Expand Down Expand Up @@ -396,13 +399,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
// When disabling centerContent, reset inset to prop value
// (enabling is handled automatically by the setCenterContent: setter)
if (oldScrollViewProps.centerContent && !newScrollViewProps.centerContent) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
((RCTEnhancedScrollView *)_scrollView).baseContentInset =
RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}

// Only apply contentInset from props if centerContent is disabled
// When centerContent is enabled, the inset is calculated by centerContentIfNeeded
if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset && !newScrollViewProps.centerContent) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
((RCTEnhancedScrollView *)_scrollView).baseContentInset =
RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}
if (oldScrollViewProps.contentOffset != newScrollViewProps.contentOffset) {
_scrollView.contentOffset = RCTCGPointFromPoint(newScrollViewProps.contentOffset);
Expand Down Expand Up @@ -445,6 +450,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
}
_shouldUpdateContentInsetAdjustmentBehavior = NO;
}
scrollView.padsContentForSafeArea = newScrollViewProps.padsContentForSafeArea;

MAP_SCROLL_VIEW_PROP(disableIntervalMomentum);
MAP_SCROLL_VIEW_PROP(snapToInterval);
Expand Down Expand Up @@ -595,7 +601,9 @@ - (BOOL)_shouldDisableScrollInteraction
auto metrics = ScrollViewEventEmitter::Metrics{};
metrics.contentSize = RCTSizeFromCGSize(_scrollView.contentSize);
metrics.contentOffset = RCTPointFromCGPoint(_scrollView.contentOffset);
metrics.contentInset = RCTEdgeInsetsFromUIEdgeInsets(_scrollView.contentInset);
auto *scrollView = (RCTEnhancedScrollView *)_scrollView;
metrics.contentInset = RCTEdgeInsetsFromUIEdgeInsets(
scrollView.padsContentForSafeArea ? scrollView.baseContentInset : scrollView.contentInset);
metrics.containerSize = RCTSizeFromCGSize(_scrollView.bounds.size);
metrics.zoomScale = _scrollView.zoomScale;
metrics.timestamp = CACurrentMediaTime();
Expand Down Expand Up @@ -699,11 +707,12 @@ - (void)prepareForRecycle
// Invalidate cached content size so that updateState: recalculates the
// container frame after zoomScale reset (which may have mutated it in RTL).
_contentSize = CGSizeZero;
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(props.contentInset);
((RCTEnhancedScrollView *)_scrollView).baseContentInset = RCTUIEdgeInsetsFromEdgeInsets(props.contentInset);
// We set the default behavior to "never" so that iOS
// doesn't do weird things to UIScrollView insets automatically
// and keeps it as an opt-in behavior.
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
((RCTEnhancedScrollView *)_scrollView).padsContentForSafeArea = NO;
_shouldUpdateContentInsetAdjustmentBehavior = YES;
_automaticallyAdjustKeyboardInsets = NO;
_isUserTriggeredScrolling = NO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ BaseScrollViewProps::BaseScrollViewProps(
"horizontal",
sourceProps.horizontal,
true)),
padsContentForSafeArea(convertRawProp(
context,
rawProps,
"padsContentForSafeArea",
sourceProps.padsContentForSafeArea,
false)),
scrollEventThrottle(convertRawProp(
context,
rawProps,
Expand Down Expand Up @@ -294,6 +300,7 @@ void BaseScrollViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(showsVerticalScrollIndicator);
RAW_SET_PROP_SWITCH_CASE_BASIC(persistentScrollbar);
RAW_SET_PROP_SWITCH_CASE_BASIC(horizontal);
RAW_SET_PROP_SWITCH_CASE_BASIC(padsContentForSafeArea);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEventThrottle);
RAW_SET_PROP_SWITCH_CASE_BASIC(zoomScale);
RAW_SET_PROP_SWITCH_CASE_BASIC(contentInset);
Expand Down Expand Up @@ -405,6 +412,10 @@ SharedDebugStringConvertibleList BaseScrollViewProps::getDebugProps() const {
defaultScrollViewProps.persistentScrollbar),
debugStringConvertibleItem(
"horizontal", horizontal, defaultScrollViewProps.horizontal),
debugStringConvertibleItem(
"padsContentForSafeArea",
padsContentForSafeArea,
defaultScrollViewProps.padsContentForSafeArea),
debugStringConvertibleItem(
"scrollEventThrottle",
scrollEventThrottle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class BaseScrollViewProps : public ViewProps {
bool showsVerticalScrollIndicator{true};
bool persistentScrollbar{false};
bool horizontal{false};
bool padsContentForSafeArea{false};
Float scrollEventThrottle{};
Float zoomScale{1.0f};
EdgeInsets contentInset{};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,57 @@
* LICENSE file in the root directory of this source tree.
*/

#include <react/renderer/components/scrollview/BaseScrollViewProps.h>
#include <react/renderer/components/scrollview/ScrollViewState.h>
#include <react/renderer/core/RawPropsParser.h>

#include <gtest/gtest.h>

namespace facebook::react {

namespace {

BaseScrollViewProps parseScrollViewProps(
const folly::dynamic& value,
const BaseScrollViewProps& sourceProps = BaseScrollViewProps{}) {
ContextContainer contextContainer{};
PropsParserContext context{-1, contextContainer};
RawProps raw{value};
RawPropsParser parser{};
parser.prepare<BaseScrollViewProps>();
raw.parse(parser);
return {context, sourceProps, raw};
}

} // namespace

TEST(ScrollViewPropsTest, safeAreaPaddingRequiresExplicitOptIn) {
EXPECT_FALSE(BaseScrollViewProps{}.padsContentForSafeArea);
for (const auto* behavior :
{"never", "automatic", "scrollableAxes", "always"}) {
auto props = parseScrollViewProps(
folly::dynamic::object("contentInsetAdjustmentBehavior", behavior));
EXPECT_FALSE(props.padsContentForSafeArea);
}
}

TEST(ScrollViewPropsTest, safeAreaPaddingCanBeEnabledAndDisabled) {
auto enabled = parseScrollViewProps(
folly::dynamic::object("padsContentForSafeArea", true));
EXPECT_TRUE(enabled.padsContentForSafeArea);
auto disabled = parseScrollViewProps(
folly::dynamic::object("padsContentForSafeArea", false), enabled);
EXPECT_FALSE(disabled.padsContentForSafeArea);
}

TEST(ScrollViewPropsTest, safeAreaPaddingResetsToDefault) {
auto enabled = parseScrollViewProps(
folly::dynamic::object("padsContentForSafeArea", true));
auto reset = parseScrollViewProps(
folly::dynamic::object("padsContentForSafeArea", nullptr), enabled);
EXPECT_FALSE(reset.padsContentForSafeArea);
}

TEST(ScrollViewStateTest, defaultConstructor) {
ScrollViewState state;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool disableIntervalMomentum;
public bool horizontal;
public bool isInvertedVirtualizedList;
public bool padsContentForSafeArea;
public bool pagingEnabled;
public bool persistentScrollbar;
public bool pinchGestureEnabled;
Expand Down
3 changes: 3 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1023,11 +1023,14 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
interface RCTEnhancedScrollView : public UIScrollView {
public @property (assign) BOOL centerContent;
public @property (assign) BOOL disableIntervalMomentum;
public @property (assign) BOOL horizontal;
public @property (assign) BOOL inverted;
public @property (assign) BOOL padsContentForSafeArea;
public @property (assign) BOOL pinchGestureEnabled;
public @property (assign) BOOL snapToEnd;
public @property (assign) BOOL snapToStart;
public @property (assign) CGFloat snapToInterval;
public @property (assign) UIEdgeInsets baseContentInset;
public @property (copy) NSArray<NSNumber*>* snapToOffsets;
public @property (copy) NSString* snapToAlignment;
public @property (strong, readonly) RCTGenericDelegateSplitter<id<UIScrollViewDelegate>>* delegateSplitter;
Expand Down
4 changes: 4 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1032,11 +1032,14 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
interface RCTEnhancedScrollView : public UIScrollView {
public @property (assign) BOOL centerContent;
public @property (assign) BOOL disableIntervalMomentum;
public @property (assign) BOOL horizontal;
public @property (assign) BOOL inverted;
public @property (assign) BOOL padsContentForSafeArea;
public @property (assign) BOOL pinchGestureEnabled;
public @property (assign) BOOL snapToEnd;
public @property (assign) BOOL snapToStart;
public @property (assign) CGFloat snapToInterval;
public @property (assign) UIEdgeInsets baseContentInset;
public @property (copy) NSArray<NSNumber*>* snapToOffsets;
public @property (copy) NSString* snapToAlignment;
public @property (strong, readonly) RCTGenericDelegateSplitter<id<UIScrollViewDelegate>>* delegateSplitter;
Expand Down Expand Up @@ -3981,6 +3984,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool disableIntervalMomentum;
public bool horizontal;
public bool isInvertedVirtualizedList;
public bool padsContentForSafeArea;
public bool pagingEnabled;
public bool persistentScrollbar;
public bool pinchGestureEnabled;
Expand Down
3 changes: 3 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1023,11 +1023,14 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
interface RCTEnhancedScrollView : public UIScrollView {
public @property (assign) BOOL centerContent;
public @property (assign) BOOL disableIntervalMomentum;
public @property (assign) BOOL horizontal;
public @property (assign) BOOL inverted;
public @property (assign) BOOL padsContentForSafeArea;
public @property (assign) BOOL pinchGestureEnabled;
public @property (assign) BOOL snapToEnd;
public @property (assign) BOOL snapToStart;
public @property (assign) CGFloat snapToInterval;
public @property (assign) UIEdgeInsets baseContentInset;
public @property (copy) NSArray<NSNumber*>* snapToOffsets;
public @property (copy) NSString* snapToAlignment;
public @property (strong, readonly) RCTGenericDelegateSplitter<id<UIScrollViewDelegate>>* delegateSplitter;
Expand Down
3 changes: 3 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1023,11 +1023,14 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
interface RCTEnhancedScrollView : public UIScrollView {
public @property (assign) BOOL centerContent;
public @property (assign) BOOL disableIntervalMomentum;
public @property (assign) BOOL horizontal;
public @property (assign) BOOL inverted;
public @property (assign) BOOL padsContentForSafeArea;
public @property (assign) BOOL pinchGestureEnabled;
public @property (assign) BOOL snapToEnd;
public @property (assign) BOOL snapToStart;
public @property (assign) CGFloat snapToInterval;
public @property (assign) UIEdgeInsets baseContentInset;
public @property (copy) NSArray<NSNumber*>* snapToOffsets;
public @property (copy) NSString* snapToAlignment;
public @property (strong, readonly) RCTGenericDelegateSplitter<id<UIScrollViewDelegate>>* delegateSplitter;
Expand Down
1 change: 1 addition & 0 deletions scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,7 @@ class facebook::react::BaseScrollViewProps : public facebook::react::HostPlatfor
public bool disableIntervalMomentum;
public bool horizontal;
public bool isInvertedVirtualizedList;
public bool padsContentForSafeArea;
public bool pagingEnabled;
public bool persistentScrollbar;
public bool pinchGestureEnabled;
Expand Down
Loading