diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index ed1ba1b01699e6..b685c94acbfa68 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -368,6 +368,13 @@ export type Props = $ReadOnly<{| * ``` */ contentContainerStyle?: ?ViewStyleProp, + /** + * When true, the scroll view stops on the next index (in relation to scroll + * position at release) regardless of how fast the gesture is. This can be + * used for horizontal pagination when the page is less than the width of + * the ScrollView. The default value is false. + */ + disableIntervalMomentum?: ?boolean, /** * A floating-point number that determines how quickly the scroll view * decelerates after the user lifts their finger. You may also use string diff --git a/React/Views/ScrollView/RCTScrollView.h b/React/Views/ScrollView/RCTScrollView.h index 6ef40fa840857d..55b5e3a3490fbe 100644 --- a/React/Views/ScrollView/RCTScrollView.h +++ b/React/Views/ScrollView/RCTScrollView.h @@ -46,6 +46,7 @@ @property (nonatomic, copy) NSDictionary *maintainVisibleContentPosition; @property (nonatomic, assign) BOOL scrollToOverflowEnabled; @property (nonatomic, assign) int snapToInterval; +@property (nonatomic, assign) BOOL disableIntervalMomentum; @property (nonatomic, copy) NSArray *snapToOffsets; @property (nonatomic, assign) BOOL snapToStart; @property (nonatomic, assign) BOOL snapToEnd; diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 27347c05036d32..2427f786b04da8 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -855,7 +855,11 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi // What is the current offset? CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y; - CGFloat targetContentOffsetAlongAxis = isHorizontal ? targetContentOffset->x : targetContentOffset->y; + CGFloat targetContentOffsetAlongAxis = targetContentOffset->y; + if (isHorizontal) { + // Use current scroll offset to determine the next index to snap to when momentum disabled + targetContentOffsetAlongAxis = self.disableIntervalMomentum ? scrollView.contentOffset.x : targetContentOffset->x; + } // Offset based on desired alignment CGFloat frameLength = isHorizontal ? self.frame.size.width : self.frame.size.height; @@ -868,6 +872,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi // Pick snap point based on direction and proximity CGFloat fractionalIndex = (targetContentOffsetAlongAxis + alignmentOffset) / snapToIntervalF; + NSInteger snapIndex = velocityAlongAxis > 0.0 ? ceil(fractionalIndex) : diff --git a/React/Views/ScrollView/RCTScrollViewManager.m b/React/Views/ScrollView/RCTScrollViewManager.m index 1b4c52e4110b9a..bed6d8435388e2 100644 --- a/React/Views/ScrollView/RCTScrollViewManager.m +++ b/React/Views/ScrollView/RCTScrollViewManager.m @@ -85,6 +85,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int) +RCT_EXPORT_VIEW_PROPERTY(disableIntervalMomentum, BOOL) RCT_EXPORT_VIEW_PROPERTY(snapToOffsets, NSArray) RCT_EXPORT_VIEW_PROPERTY(snapToStart, BOOL) RCT_EXPORT_VIEW_PROPERTY(snapToEnd, BOOL) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index db8e726a29c506..18687228c13142 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -65,6 +65,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements private @Nullable String mScrollPerfTag; private @Nullable Drawable mEndBackground; private int mEndFillColor = Color.TRANSPARENT; + private boolean mDisableIntervalMomentum = false; private int mSnapInterval = 0; private float mDecelerationRate = 0.985f; private @Nullable List mSnapOffsets; @@ -141,6 +142,10 @@ public boolean getRemoveClippedSubviews() { return mRemoveClippedSubviews; } + public void setDisableIntervalMomentum(boolean disableIntervalMomentum) { + mDisableIntervalMomentum = disableIntervalMomentum; + } + public void setSendMomentumEvents(boolean sendMomentumEvents) { mSendMomentumEvents = sendMomentumEvents; } @@ -579,6 +584,10 @@ private void flingAndSnap(int velocityX) { int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth()); int targetOffset = predictFinalScrollPosition(velocityX); + if (mDisableIntervalMomentum) { + targetOffset = getScrollX(); + } + int smallerOffset = 0; int largerOffset = maximumOffset; int firstOffset = 0; 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 1b84dee6b84f37..0a5d1aa12c7053 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 @@ -80,6 +80,11 @@ public void setDecelerationRate(ReactHorizontalScrollView view, float decelerati view.setDecelerationRate(decelerationRate); } + @ReactProp(name = "disableIntervalMomentum") + public void setDisableIntervalMomentum(ReactHorizontalScrollView view, boolean disbaleIntervalMomentum) { + view.setDisableIntervalMomentum(disbaleIntervalMomentum); + } + @ReactProp(name = "snapToInterval") public void setSnapToInterval(ReactHorizontalScrollView view, float snapToInterval) { // snapToInterval needs to be exposed as a float because of the Javascript interface.