diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 36f714228042..88c098fb4462 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -438,6 +438,21 @@ var ScrollResponderMixin = { ); }, + /** + * A helper function to scroll by a specific offset in the ScrollView. + * This is useful when you want to change the size of the content view and scroll + * position at the same time. Syntax: + * + * scrollResponderScrollBy(options: {deltaX: number = 0; deltaY: number = 0; animated: boolean = true}) + */ + scrollResponderScrollBy: function(options : {deltaX?: number, deltaY?: number, animated?: boolean}) { + UIManager.dispatchViewManagerCommand( + this.scrollResponderGetScrollableNode(), + UIManager.RCTScrollView.Commands.scrollBy, + [options.deltaX || 0, options.deltaY || 0, options.animated !== false], + ); + }, + /** * Deprecated, do not use. */ diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 1787476babc0..dd2ceffb0f09 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -530,6 +530,19 @@ const ScrollView = createReactClass({ }); }, + /** + * Scrolls by a given offset, either immediately or with a smooth animation. + * + * Syntax: + * + * `scrollBy(options: {deltaX: number = 0; deltaY: number = 0; animated: boolean = true})` + */ + scrollBy: function(options: { deltaX?: number, deltaY?: number, animated?: boolean } ) { + const data = {deltaX: options.deltaX || 0, deltaY: options.deltaY || 0, + animated: options.animated !== false}; + this.getScrollResponder().scrollResponderScrollBy(data); + }, + /** * Deprecated, use `scrollTo` instead. */ diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 18e5546d1e96..4fe76507d34a 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -587,6 +587,17 @@ - (void)scrollToEnd:(BOOL)animated } } +- (void)scrollByOffset:(CGPoint)offset animated:(BOOL)animated +{ + if (offset.x != 0 || offset.y != 0) { + // Ensure at least one scroll event will fire + _allowNextScrollNoMatterWhat = YES; + CGPoint oldOffset = _scrollView.contentOffset; + CGPoint newOffset = (CGPoint){oldOffset.x + offset.x, oldOffset.y + offset.y}; + [_scrollView setContentOffset:newOffset animated:animated]; + } +} + - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { [_scrollView zoomToRect:rect animated:animated]; diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index efa162bc77e0..32b927f812cb 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -175,6 +175,23 @@ - (UIView *)view }]; } +RCT_EXPORT_METHOD(scrollBy:(nonnull NSNumber *)reactTag + deltaX:(CGFloat)deltaX + deltaY:(CGFloat)deltaY + animated:(BOOL)animated) +{ + [self.bridge.uiManager addUIBlock: + ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry){ + UIView *view = viewRegistry[reactTag]; + if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { + [(id)view scrollByOffset:(CGPoint){deltaX, deltaY} animated:animated]; + } else { + RCTLogError(@"tried to scrollBy: on non-RCTScrollableProtocol view %@ " + "with tag #%@", view, reactTag); + } + }]; +} + RCT_EXPORT_METHOD(zoomToRect:(nonnull NSNumber *)reactTag withRect:(CGRect)rect animated:(BOOL)animated) diff --git a/React/Views/RCTScrollableProtocol.h b/React/Views/RCTScrollableProtocol.h index a143d638602f..a3e3ea8042ec 100644 --- a/React/Views/RCTScrollableProtocol.h +++ b/React/Views/RCTScrollableProtocol.h @@ -19,6 +19,7 @@ - (void)scrollToOffset:(CGPoint)offset; - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated; +- (void)scrollByOffset:(CGPoint)offset animated:(BOOL)animated; /** * If this is a vertical scroll view, scrolls to the bottom. * If this is a horizontal scroll view, scrolls to the right.