From 7c77b24ed0bab475ee071e5542f01f7b64325788 Mon Sep 17 00:00:00 2001 From: Guilherme Iscaro Date: Sun, 23 Dec 2018 18:19:34 -0200 Subject: [PATCH] Add allowScrollOutOfBounds to ScrollView When using the scrollTo() function the user can specify a x/y position that tells the ScrollView to scroll to. Prior to this patch the x/y were not validated which could cause the SrollView to scroll to a position outside the content view bounds, leaving the screen empty (the content disapears, since it goes outside the screen). In order to fix this problem, this commit imposes that the x/y must be inside the view bounds if allowScrollOutOfBounds is set to false. This new prop is also set to true automatically to not break existing apps which may depends on this feature. Note that this behavior does not occur on Android, because the Android's framework itself implements this features via [1] and [2], which are used by React Native. [1]: https://developer.android.com/reference/android/widget/ScrollView#smoothScrollTo(int,%20int) [2]: https://developer.android.com/reference/android/widget/ScrollView.html#scrollTo(int,%20int) --- Libraries/Components/ScrollView/ScrollView.js | 10 +++++ React/Views/ScrollView/RCTScrollView.h | 1 + React/Views/ScrollView/RCTScrollView.m | 42 ++++++++++++++----- React/Views/ScrollView/RCTScrollViewManager.m | 1 + 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 8a238edaebf735..cebbb1f0c10726 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -75,6 +75,12 @@ type IOSProps = $ReadOnly<{| * @platform ios */ automaticallyAdjustContentInsets?: ?boolean, + /** + * Set to false if the scroll should be clamped to the bounds of the + * ScrollView children when using the scrollTo() function. The default value is true. + * @platform ios + */ + allowScrollOutOfBounds?: ?boolean, /** * The amount by which the scroll view content is inset from the edges * of the scroll view. Defaults to `{top: 0, left: 0, bottom: 0, right: 0}`. @@ -976,6 +982,10 @@ class ScrollView extends React.Component { : styles.baseVertical; const props = { ...this.props, + allowScrollOutOfBounds: + this.props.allowScrollOutOfBounds !== undefined + ? this.props.allowScrollOutOfBounds + : true, alwaysBounceHorizontal, alwaysBounceVertical, style: ([baseStyle, this.props.style]: ?Array), diff --git a/React/Views/ScrollView/RCTScrollView.h b/React/Views/ScrollView/RCTScrollView.h index 3404422c7b00da..74af0678ab74b7 100644 --- a/React/Views/ScrollView/RCTScrollView.h +++ b/React/Views/ScrollView/RCTScrollView.h @@ -38,6 +38,7 @@ */ @property (nonatomic, readonly) UIScrollView *scrollView; +@property (nonatomic, assign) BOOL allowScrollOutOfBounds; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, assign) BOOL DEPRECATED_sendUpdatedChildFrames; diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 58bff0f0fd2c25..b96896009a3455 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -585,12 +585,40 @@ - (void)scrollToOffset:(CGPoint)offset [self scrollToOffset:offset animated:YES]; } +- (CGPoint)getMaxXOffset +{ + CGFloat offsetX = _scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right; + return CGPointMake(fmax(offsetX, 0), 0); +} + +- (CGPoint)getMaxYOffset +{ + CGFloat offsetY = _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom; + return CGPointMake(0, fmax(offsetY, 0)); +} + +- (CGPoint)getMaxOffset +{ + return [self isHorizontal:_scrollView] ? [self getMaxXOffset] : [self getMaxYOffset]; +} + - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated { - if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) { + CGPoint maxX = [self getMaxXOffset]; + CGPoint maxY = [self getMaxYOffset]; + CGPoint toOffset; + + if (self.allowScrollOutOfBounds) { + toOffset = offset; + } else { + // Restrict inside the scroll view container bounds + toOffset = CGPointMake(fmax(0, fmin(offset.x, maxX.x)), fmax(0, fmin(offset.y, maxY.y))); + } + + if (!CGPointEqualToPoint(_scrollView.contentOffset, toOffset)) { // Ensure at least one scroll event will fire _allowNextScrollNoMatterWhat = YES; - [_scrollView setContentOffset:offset animated:animated]; + [_scrollView setContentOffset:toOffset animated:animated]; } } @@ -600,15 +628,7 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated */ - (void)scrollToEnd:(BOOL)animated { - BOOL isHorizontal = [self isHorizontal:_scrollView]; - CGPoint offset; - if (isHorizontal) { - CGFloat offsetX = _scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right; - offset = CGPointMake(fmax(offsetX, 0), 0); - } else { - CGFloat offsetY = _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom; - offset = CGPointMake(0, fmax(offsetY, 0)); - } + CGPoint offset = [self getMaxOffset]; if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) { // Ensure at least one scroll event will fire _allowNextScrollNoMatterWhat = YES; diff --git a/React/Views/ScrollView/RCTScrollViewManager.m b/React/Views/ScrollView/RCTScrollViewManager.m index 6494d52f48fb29..f6fd69093f6290 100644 --- a/React/Views/ScrollView/RCTScrollViewManager.m +++ b/React/Views/ScrollView/RCTScrollViewManager.m @@ -54,6 +54,7 @@ - (UIView *)view return [[RCTScrollView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } +RCT_EXPORT_VIEW_PROPERTY(allowScrollOutOfBounds, BOOL) RCT_EXPORT_VIEW_PROPERTY(alwaysBounceHorizontal, BOOL) RCT_EXPORT_VIEW_PROPERTY(alwaysBounceVertical, BOOL) RCT_EXPORT_VIEW_PROPERTY(bounces, BOOL)