diff --git a/Examples/UIExplorer/js/ScrollViewExample.js b/Examples/UIExplorer/js/ScrollViewExample.js index 7eb41692070..4d5dc6bcf90 100644 --- a/Examples/UIExplorer/js/ScrollViewExample.js +++ b/Examples/UIExplorer/js/ScrollViewExample.js @@ -57,6 +57,11 @@ exports.examples = [ onPress={() => { _scrollView.scrollTo({y: 0}); }}> Scroll to top + { _scrollView.scrollToEnd({animated: true}); }}> + Scroll to bottom + ); } @@ -79,6 +84,11 @@ exports.examples = [ onPress={() => { _scrollView.scrollTo({x: 0}); }}> Scroll to start + { _scrollView.scrollToEnd({animated: true}); }}> + Scroll to end + ); } diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 5bd9d93f281..aec5a260ed2 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -377,11 +377,11 @@ var ScrollResponderMixin = { }, /** - * A helper function to scroll to a specific point in the scrollview. - * This is currently used to help focus on child textviews, but can also + * A helper function to scroll to a specific point in the ScrollView. + * This is currently used to help focus child TextViews, but can also * be used to quickly scroll to any element we want to focus. Syntax: * - * scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true}) + * `scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})` * * Note: The weird argument signature is due to the fact that, for historical reasons, * the function also accepts separate arguments as as alternative to the options object. @@ -404,6 +404,32 @@ var ScrollResponderMixin = { ); }, + /** + * Scrolls to the end of the ScrollView, either immediately or with a smooth + * animation. + * + * Example: + * + * `scrollResponderScrollToEnd({animated: true})` + */ + scrollResponderScrollToEnd: function( + options?: { animated?: boolean }, + ) { + if (Platform.OS !== 'ios') { + console.warn( + 'scrollResponderScrollToEnd is not supported on this platform' + ); + return; + } + // Default to true + const animated = (options && options.animated) !== false; + UIManager.dispatchViewManagerCommand( + this.scrollResponderGetScrollableNode(), + UIManager.RCTScrollView.Commands.scrollToEnd, + [animated], + ); + }, + /** * Deprecated, do not use. */ diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index a53ff84ec9f..04dfc3607a9 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -382,11 +382,11 @@ const ScrollView = React.createClass({ /** * Scrolls to a given x, y offset, either immediately or with a smooth animation. * - * Syntax: + * Example: * - * `scrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})` + * `scrollTo({x: 0; y: 0; animated: true})` * - * Note: The weird argument signature is due to the fact that, for historical reasons, + * Note: The weird function signature is due to the fact that, for historical reasons, * the function also accepts separate arguments as as alternative to the options object. * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED. */ @@ -404,7 +404,39 @@ const ScrollView = React.createClass({ }, /** - * Deprecated, do not use. + * If this is a vertical ScrollView scrolls to the bottom. + * If this is a horizontal ScrollView scrolls to the right. + * + * Use `scrollToEnd({animated: true})` for smooth animated scrolling, + * `scrollToEnd({animated: false})` for immediate scrolling. + * If no options are passed, `animated` defaults to true. + * + * See `ScrollView#scrollToEnd`. + */ + scrollToEnd: function( + options?: { animated?: boolean }, + ) { + // Default to true + const animated = (options && options.animated) !== false; + if (Platform.OS === 'ios') { + this.getScrollResponder().scrollResponderScrollToEnd({ + animated: animated, + }); + } else if (Platform.OS === 'android') { + // On Android scrolling past the end of the ScrollView gets clipped + // - scrolls to the end. + if (this.props.horizontal) { + this.scrollTo({x: 10*1000*1000, animated: animated}); + } else { + this.scrollTo({y: 10*1000*1000, animated: animated}); + } + } else { + console.warn('scrollToEnd is not supported on this platform'); + } + }, + + /** + * Deprecated, use `scrollTo` instead. */ scrollWithoutAnimationTo: function(y: number = 0, x: number = 0) { console.warn('`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead'); diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 81e5aadd2bb..0468fbe8158 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -287,6 +287,29 @@ var ListView = React.createClass({ } }, + /** + * If this is a vertical ListView scrolls to the bottom. + * If this is a horizontal ListView scrolls to the right. + * + * Use `scrollToEnd({animated: true})` for smooth animated scrolling, + * `scrollToEnd({animated: false})` for immediate scrolling. + * If no options are passed, `animated` defaults to true. + * + * See `ScrollView#scrollToEnd`. + */ + scrollToEnd: function(options?: { animated?: boolean }) { + if (this._scrollComponent) { + if (this._scrollComponent.scrollToEnd) { + this._scrollComponent.scrollToEnd(options); + } else { + console.warn( + 'The scroll component used by the ListView does not support ' + + 'scrollToEnd. Check the renderScrollComponent prop of your ListView.' + ); + } + } + }, + setNativeProps: function(props: Object) { if (this._scrollComponent) { this._scrollComponent.setNativeProps(props); diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index 5f5c9e2dc2b..aa85fa50c5b 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -480,10 +480,10 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray self.frame.size.width; +} + - (void)scrollToOffset:(CGPoint)offset { [self scrollToOffset:offset animated:YES]; @@ -602,6 +607,26 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } } +/** + * If this is a vertical scroll view, scrolls to the bottom. + * If this is a horizontal scroll view, scrolls to the right. + */ +- (void)scrollToEnd:(BOOL)animated +{ + BOOL isHorizontal = [self isHorizontal:_scrollView]; + CGPoint offset; + if (isHorizontal) { + offset = CGPointMake(_scrollView.contentSize.width - _scrollView.bounds.size.width, 0); + } else { + offset = CGPointMake(0, _scrollView.contentSize.height - _scrollView.bounds.size.height); + } + if (!CGPointEqualToPoint(_scrollView.contentOffset, offset)) { + // Ensure at least one scroll event will fire + _allowNextScrollNoMatterWhat = YES; + [_scrollView setContentOffset:offset animated:animated]; + } +} + - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { [_scrollView zoomToRect:rect animated:animated]; @@ -727,7 +752,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll) CGFloat snapToIntervalF = (CGFloat)self.snapToInterval; // Find which axis to snap - BOOL isHorizontal = (scrollView.contentSize.width > self.frame.size.width); + BOOL isHorizontal = [self isHorizontal:scrollView]; // What is the current offset? CGFloat targetContentOffsetAlongAxis = isHorizontal ? targetContentOffset->x : targetContentOffset->y; diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 91f5634f01c..229450e76fa 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -147,6 +147,21 @@ RCT_EXPORT_METHOD(scrollTo:(nonnull NSNumber *)reactTag }]; } +RCT_EXPORT_METHOD(scrollToEnd:(nonnull NSNumber *)reactTag + animated:(BOOL)animated) +{ + [self.bridge.uiManager addUIBlock: + ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry){ + UIView *view = viewRegistry[reactTag]; + if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { + [(id)view scrollToEnd:animated]; + } else { + RCTLogError(@"tried to scrollTo: 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 90c14ebd3cc..a143d638602 100644 --- a/React/Views/RCTScrollableProtocol.h +++ b/React/Views/RCTScrollableProtocol.h @@ -19,6 +19,11 @@ - (void)scrollToOffset:(CGPoint)offset; - (void)scrollToOffset:(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. + */ +- (void)scrollToEnd:(BOOL)animated; - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated; - (void)addScrollListener:(NSObject *)scrollListener;