From ad8cbb6deab2d20bcdbddbc41322bb27eb4f8c2e Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 30 Jan 2017 10:15:18 -0800 Subject: [PATCH] Support ScrollView.scrollToEnd on Android natively Summary: This is a followup for https://github.com/facebook/react-native/pull/12088 and implements the scrolling to end on Android natively rather than sending a large scroll offset from JS. This turned out to be an OK amount of code, and some reduction in the amount of JavaScript. The only part I'm not particularly happy about is: ``` // ScrollView always has one child - the scrollable area int bottom = scrollView.getChildAt(0).getHeight() + scrollView.getPaddingBottom(); ``` According to multiple sources (e.g. [this SO answer](http://stackoverflow.com/questions/3609297/android-total-height-of-scrollview)) it is the way to get the total size of the scrollable area, similar to`scrollView.contentSize` on iOS but more ugly and relying on the fact the ScrollView always has a single child (hopefully this won't change in future versions of Android). An alternative is: ``` View lastChild = scrollLayout.getChildAt(scrollLayout.getChildCount() - 1); int bottom = lastChild.getBottom() + scrollLayout.getPadd Closes https://github.com/facebook/react-native/pull/12101 Differential Revision: D4481523 Pulled By: mkonicek fbshipit-source-id: 8c7967a0b9e06890c1e1ea70ad573c6eceb03daf --- Libraries/Components/ScrollResponder.js | 6 ------ Libraries/Components/ScrollView/ScrollView.js | 20 +++---------------- .../RecyclerViewBackedScrollViewManager.java | 9 +++++++++ .../ReactHorizontalScrollViewManager.java | 14 +++++++++++++ .../scroll/ReactScrollViewCommandHelper.java | 20 ++++++++++++++++++- .../views/scroll/ReactScrollViewManager.java | 14 +++++++++++++ 6 files changed, 59 insertions(+), 24 deletions(-) diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index aec5a260e..955d7ea43 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -415,12 +415,6 @@ var ScrollResponderMixin = { 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( diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 04dfc3607..1c7b704c8 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -410,29 +410,15 @@ const ScrollView = React.createClass({ * 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'); - } + this.getScrollResponder().scrollResponderScrollToEnd({ + animated: animated, + }); }, /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java index b82c03f74..ca4586d76 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java @@ -80,6 +80,15 @@ public class RecyclerViewBackedScrollViewManager extends scrollView.scrollTo(data.mDestX, data.mDestY, data.mAnimated); } + @Override + public void scrollToEnd( + RecyclerViewBackedScrollView scrollView, + ReactScrollViewCommandHelper.ScrollToEndCommandData data) { + // Not implemented. + // RecyclerViewBackedScrollView is deprecated and will be removed. + // People should use a standard ScrollView or ListView instead. + } + @Override public @Nullable Map getExportedCustomDirectEventTypeConstants() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index 2865a94c5..769a90738 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -117,6 +117,20 @@ public class ReactHorizontalScrollViewManager } } + @Override + public void scrollToEnd( + ReactHorizontalScrollView scrollView, + ReactScrollViewCommandHelper.ScrollToEndCommandData data) { + // ScrollView always has one child - the scrollable area + int right = + scrollView.getChildAt(0).getWidth() + scrollView.getPaddingRight(); + if (data.mAnimated) { + scrollView.smoothScrollTo(right, scrollView.getScrollY()); + } else { + scrollView.scrollTo(right, scrollView.getScrollY()); + } + } + /** * When set, fills the rest of the scrollview with a color to avoid setting a background and * creating unnecessary overdraw. diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java index aa9560df0..af97a8bba 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewCommandHelper.java @@ -25,9 +25,11 @@ import com.facebook.react.common.MapBuilder; public class ReactScrollViewCommandHelper { public static final int COMMAND_SCROLL_TO = 1; + public static final int COMMAND_SCROLL_TO_END = 2; public interface ScrollCommandHandler { void scrollTo(T scrollView, ScrollToCommandData data); + void scrollToEnd(T scrollView, ScrollToEndCommandData data); } public static class ScrollToCommandData { @@ -42,10 +44,21 @@ public class ReactScrollViewCommandHelper { } } + public static class ScrollToEndCommandData { + + public final boolean mAnimated; + + ScrollToEndCommandData(boolean animated) { + mAnimated = animated; + } + } + public static Map getCommandsMap() { return MapBuilder.of( "scrollTo", - COMMAND_SCROLL_TO); + COMMAND_SCROLL_TO, + "scrollToEnd", + COMMAND_SCROLL_TO_END); } public static void receiveCommand( @@ -64,6 +77,11 @@ public class ReactScrollViewCommandHelper { viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY, animated)); return; } + case COMMAND_SCROLL_TO_END: { + boolean animated = args.getBoolean(0); + viewManager.scrollToEnd(scrollView, new ScrollToEndCommandData(animated)); + return; + } default: throw new IllegalArgumentException(String.format( "Unsupported command %d received by %s.", diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index ba9173bcc..76cc41ac9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -131,6 +131,20 @@ public class ReactScrollViewManager } } + @Override + public void scrollToEnd( + ReactScrollView scrollView, + ReactScrollViewCommandHelper.ScrollToEndCommandData data) { + // ScrollView always has one child - the scrollable area + int bottom = + scrollView.getChildAt(0).getHeight() + scrollView.getPaddingBottom(); + if (data.mAnimated) { + scrollView.smoothScrollTo(scrollView.getScrollX(), bottom); + } else { + scrollView.scrollTo(scrollView.getScrollX(), bottom); + } + } + @Override public @Nullable Map getExportedCustomDirectEventTypeConstants() { return createExportedCustomDirectEventTypeConstants();