Add scrollToEnd to ScrollView and ListView

Summary:
**Motivation**

A basic task of making a React Native ScrollView or ListView scroll to the bottom is currently very hard to accomplish:
- https://github.com/facebook/react-native/issues/8003
- https://github.com/facebook/react-native/issues/913
- http://stackoverflow.com/questions/29829375/how-to-scroll-to-bottom-in-react-native-listview

**NOTE:** If you're building something like a chat app where you want a ListView to keep scrolling to the bottom at all times, it's easiest to use the [react-native-invertible-scrollview](https://github.com/exponent/react-native-invertible-scroll-view) component rather calling `scrollToEnd` manually when layout changes. The invertible-scrollview uses a clever trick to invert the direction of the ScrollView.

This pull request adds a `scrollToEnd` method which scrolls to the bottom if the ScrollView is vertical, to the right if the ScrollView is horizontal.

The implementation is based on this SO answer:
http://stackoverflow.com/questions/952412/uiscrollview-scrol
Closes https://github.com/facebook/react-native/pull/12088

Differential Revision: D4474974

Pulled By: mkonicek

fbshipit-source-id: 6ecf8b3435f47dd3a31e2fd5be6859062711c233
This commit is contained in:
Martin Konicek 2017-01-27 10:09:20 -08:00 committed by Facebook Github Bot
parent 81fe1a3618
commit 9dee696ed8
8 changed files with 148 additions and 12 deletions

View File

@ -57,6 +57,11 @@ exports.examples = [
onPress={() => { _scrollView.scrollTo({y: 0}); }}>
<Text>Scroll to top</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => { _scrollView.scrollToEnd({animated: true}); }}>
<Text>Scroll to bottom</Text>
</TouchableOpacity>
</View>
);
}
@ -79,6 +84,11 @@ exports.examples = [
onPress={() => { _scrollView.scrollTo({x: 0}); }}>
<Text>Scroll to start</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => { _scrollView.scrollToEnd({animated: true}); }}>
<Text>Scroll to end</Text>
</TouchableOpacity>
</View>
);
}

View File

@ -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.
*/

View File

@ -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');

View File

@ -289,6 +289,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);

View File

@ -480,10 +480,10 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument
expectedCount -= 2;
}
RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd. \
If you haven\'t changed this method yourself, this usually means that \
your versions of the native code and JavaScript code are out of sync. \
Updating both should make this error go away.",
RCTLogError(@"%@.%@ was called with %zd arguments but expects %zd arguments. "
@"If you haven\'t changed this method yourself, this usually means that "
@"your versions of the native code and JavaScript code are out of sync. "
@"Updating both should make this error go away.",
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
actualCount, expectedCount);
return nil;

View File

@ -588,6 +588,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
_scrollView.contentOffset = contentOffset;
}
- (BOOL)isHorizontal:(UIScrollView *)scrollView
{
return scrollView.contentSize.width > 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;

View File

@ -148,6 +148,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<NSNumber *, UIView *> *viewRegistry){
UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id<RCTScrollableProtocol>)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)

View File

@ -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<UIScrollViewDelegate> *)scrollListener;