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 @@ -274,6 +274,17 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
@"Unexpected implementation of UIViewAnimationCurve");
return curve << 16;
}

static inline UIEdgeInsets RCTEffectiveContentInset(UIScrollView *scrollView)
{
if (@available(iOS 11.0, *)) {
if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, scrollView.adjustedContentInset)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Miyou
Would it be safer/simpler to always prefer adjustedContentInset on iOS 11+ (without the != .zero guard), since that is UIKit’s effective geometry even when it happens to be zero?
As written, zero adjusted values fall back to contentInset, which may reintroduce mismatch in some transition states.

return scrollView.adjustedContentInset;
}
}

return scrollView.contentInset;
}
#endif

- (RCTGenericDelegateSplitter<id<UIScrollViewDelegate>> *)scrollViewDelegateSplitter
Expand Down Expand Up @@ -923,29 +934,7 @@ - (void)flashScrollIndicators

- (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated
{
CGPoint offset = CGPointMake(x, y);
CGRect maxRect = CGRectMake(
fmin(-_scrollView.contentInset.left, 0),
fmin(-_scrollView.contentInset.top, 0),
fmax(
_scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right +
fmax(_scrollView.contentInset.left, 0),
0.01),
fmax(
_scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom +
fmax(_scrollView.contentInset.top, 0),
0.01)); // Make width and height greater than 0

const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (!CGRectContainsPoint(maxRect, offset) && !props.scrollToOverflowEnabled) {
CGFloat localX = fmax(offset.x, CGRectGetMinX(maxRect));
localX = fmin(localX, CGRectGetMaxX(maxRect));
CGFloat localY = fmax(offset.y, CGRectGetMinY(maxRect));
localY = fmin(localY, CGRectGetMaxY(maxRect));
offset = CGPointMake(localX, localY);
}

[self scrollToOffset:offset animated:animated];
[self scrollToOffset:CGPointMake(x, y) animated:animated];
}

- (void)scrollToEnd:(BOOL)animated
Expand Down Expand Up @@ -1017,6 +1006,27 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated
offset.x = self.contentSize.width - _scrollView.frame.size.width - offset.x;
}

UIEdgeInsets contentInset = RCTEffectiveContentInset(_scrollView);
CGRect maxRect = CGRectMake(
fmin(-contentInset.left, 0),
fmin(-contentInset.top, 0),
fmax(
_scrollView.contentSize.width - _scrollView.bounds.size.width + contentInset.right + fmax(contentInset.left, 0),
0.01),
fmax(
_scrollView.contentSize.height - _scrollView.bounds.size.height + contentInset.bottom +
fmax(contentInset.top, 0),
0.01)); // Make width and height greater than 0

const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (!CGRectContainsPoint(maxRect, offset) && !props.scrollToOverflowEnabled) {
CGFloat localX = fmax(offset.x, CGRectGetMinX(maxRect));
localX = fmin(localX, CGRectGetMaxX(maxRect));
CGFloat localY = fmax(offset.y, CGRectGetMinY(maxRect));
localY = fmin(localY, CGRectGetMaxY(maxRect));
offset = CGPointMake(localX, localY);
}

if (CGPointEqualToPoint(_scrollView.contentOffset, offset)) {
return;
}
Expand Down
48 changes: 30 additions & 18 deletions packages/react-native/React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,17 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
return curve << 16;
}

static inline UIEdgeInsets RCTEffectiveContentInset(UIScrollView *scrollView)
{
if (@available(iOS 11.0, *)) {
if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, scrollView.adjustedContentInset)) {
return scrollView.adjustedContentInset;
}
}

return scrollView.contentInset;
}

- (void)_keyboardWillChangeFrame:(NSNotification *)notification
{
if (![self automaticallyAdjustKeyboardInsets]) {
Expand Down Expand Up @@ -570,27 +581,28 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated
offset.x = _scrollView.contentSize.width - _scrollView.frame.size.width - offset.x;
}

UIEdgeInsets contentInset = RCTEffectiveContentInset(_scrollView);
CGRect maxRect = CGRectMake(
fmin(-contentInset.left, 0),
fmin(-contentInset.top, 0),
fmax(
_scrollView.contentSize.width - _scrollView.bounds.size.width + contentInset.right + fmax(contentInset.left, 0),
0.01),
fmax(
_scrollView.contentSize.height - _scrollView.bounds.size.height + contentInset.bottom +
fmax(contentInset.top, 0),
0.01)); // Make width and height greater than 0
if (!CGRectContainsPoint(maxRect, offset) && !self.scrollToOverflowEnabled) {
CGFloat x = fmax(offset.x, CGRectGetMinX(maxRect));
x = fmin(x, CGRectGetMaxX(maxRect));
CGFloat y = fmax(offset.y, CGRectGetMinY(maxRect));
y = fmin(y, CGRectGetMaxY(maxRect));
offset = CGPointMake(x, y);
}

if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) {
CGRect maxRect = CGRectMake(
fmin(-_scrollView.contentInset.left, 0),
fmin(-_scrollView.contentInset.top, 0),
fmax(
_scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right +
fmax(_scrollView.contentInset.left, 0),
0.01),
fmax(
_scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom +
fmax(_scrollView.contentInset.top, 0),
0.01)); // Make width and height greater than 0
// Ensure at least one scroll event will fire
_allowNextScrollNoMatterWhat = YES;
if (!CGRectContainsPoint(maxRect, offset) && !self.scrollToOverflowEnabled) {
CGFloat x = fmax(offset.x, CGRectGetMinX(maxRect));
x = fmin(x, CGRectGetMaxX(maxRect));
CGFloat y = fmax(offset.y, CGRectGetMinY(maxRect));
y = fmin(y, CGRectGetMaxY(maxRect));
offset = CGPointMake(x, y);
}
[_scrollView setContentOffset:offset animated:animated];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,37 @@ component ScrollViewKeyboardInsetsExample() {
);
}

component ScrollViewKeyboardInsetsAutomaticContentInsetExample() {
return (
<ScrollView
style={styles.automaticInsetScrollView}
contentContainerStyle={styles.automaticInsetContent}
automaticallyAdjustKeyboardInsets
contentInsetAdjustmentBehavior="automatic"
keyboardDismissMode="interactive">
<Text style={styles.automaticInsetTitle}>
Focus the input near the bottom and dismiss the keyboard.
</Text>
<Text style={styles.automaticInsetDescription}>
The content should return to its original position immediately without
leaving extra space at the bottom.
</Text>
<View style={styles.automaticInsetCard}>
<Text>
This reproduces the combination of automatic keyboard insets and
automatic content inset adjustment that previously left a stale bottom
gap until the next manual scroll.
</Text>
</View>
<View style={styles.automaticInsetSpacer} />
<TextInput
placeholder="Keyboard dismissal repro"
style={styles.automaticInsetTextInput}
/>
</ScrollView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -176,6 +207,42 @@ const styles = StyleSheet.create({
fontSize: 12,
fontFamily: 'Courier',
},
automaticInsetScrollView: {
flex: 1,
backgroundColor: '#f2f5f7',
},
automaticInsetContent: {
padding: 16,
paddingBottom: 32,
gap: 16,
},
automaticInsetTitle: {
fontSize: 18,
fontWeight: '600',
},
automaticInsetDescription: {
fontSize: 15,
lineHeight: 21,
},
automaticInsetCard: {
padding: 16,
borderRadius: 12,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#d0d7de',
},
automaticInsetSpacer: {
height: 320,
},
automaticInsetTextInput: {
borderWidth: 1,
borderColor: '#999',
borderRadius: 10,
backgroundColor: '#fff',
fontSize: 18,
paddingHorizontal: 14,
paddingVertical: 12,
},
});

exports.title = 'ScrollViewKeyboardInsets';
Expand All @@ -187,4 +254,11 @@ exports.examples = [
title: '<ScrollView> automaticallyAdjustKeyboardInsets Example',
render: (): React.Node => <ScrollViewKeyboardInsetsExample />,
},
{
title:
'<ScrollView> keyboard dismissal with automatic content inset adjustment',
render: (): React.Node => (
<ScrollViewKeyboardInsetsAutomaticContentInsetExample />
),
},
] as Array<RNTesterModuleExample>;