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 d92d0784d..228f7bbdb 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 @@ -287,7 +287,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements @Override public void fling(int velocityX) { if (mPagingEnabled) { - smoothScrollAndSnap(velocityX); + flingAndSnap(velocityX); } else if (mScroller != null) { // FB SCROLLVIEW CHANGE @@ -465,7 +465,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements // Only if we have pagingEnabled and we have not snapped to the page do we // need to continue checking for the scroll. And we cause that scroll by asking for it mSnappingToPage = true; - smoothScrollAndSnap(0); + flingAndSnap(0); ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this, this, ReactScrollViewHelper.MOMENTUM_DELAY); @@ -484,23 +484,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements ReactScrollViewHelper.MOMENTUM_DELAY); } - /** - * This will smooth scroll us to the nearest snap offset point - * It currently just looks at where the content is and slides to the nearest point. - * It is intended to be run after we are done scrolling, and handling any momentum scrolling. - */ - private void smoothScrollAndSnap(int velocityX) { - if (getChildCount() <= 0) { - return; - } - - int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth()); - int targetOffset = 0; - int smallerOffset = 0; - int largerOffset = maximumOffset; - int firstOffset = 0; - int lastOffset = maximumOffset; - + private int predictFinalScrollPosition(int velocityX) { // ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's // no way to customize the scroll duration. So, we create a temporary OverScroller // so we can predict where a fling would land and snap to nearby that point. @@ -508,6 +492,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements scroller.setFriction(1.0f - mDecelerationRate); // predict where a fling would end up so we can scroll to the nearest snap offset + int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth()); int width = getWidth() - getPaddingStart() - getPaddingEnd(); scroller.fling( getScrollX(), // startX @@ -521,7 +506,76 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements width/2, // overX 0 // overY ); - targetOffset = scroller.getFinalX(); + return scroller.getFinalX(); + } + + /** + * This will smooth scroll us to the nearest snap offset point + * It currently just looks at where the content is and slides to the nearest point. + * It is intended to be run after we are done scrolling, and handling any momentum scrolling. + */ + private void smoothScrollAndSnap(int velocity) { + double interval = (double) getSnapInterval(); + double currentOffset = (double) getScrollX(); + double targetOffset = (double) predictFinalScrollPosition(velocity); + + int previousPage = (int) Math.floor(currentOffset / interval); + int nextPage = (int) Math.ceil(currentOffset / interval); + int currentPage = (int) Math.round(currentOffset / interval); + int targetPage = (int) Math.round(targetOffset / interval); + + if (velocity > 0 && nextPage == previousPage) { + nextPage ++; + } else if (velocity < 0 && previousPage == nextPage) { + previousPage --; + } + + if ( + // if scrolling towards next page + velocity > 0 && + // and the middle of the page hasn't been crossed already + currentPage < nextPage && + // and it would have been crossed after flinging + targetPage > previousPage + ) { + currentPage = nextPage; + } + else if ( + // if scrolling towards previous page + velocity < 0 && + // and the middle of the page hasn't been crossed already + currentPage > previousPage && + // and it would have been crossed after flinging + targetPage < nextPage + ) { + currentPage = previousPage; + } + + targetOffset = currentPage * interval; + if (targetOffset != currentOffset) { + mActivelyScrolling = true; + smoothScrollTo((int) targetOffset, getScrollY()); + } + } + + private void flingAndSnap(int velocityX) { + if (getChildCount() <= 0) { + return; + } + + // pagingEnabled only allows snapping one interval at a time + if (mSnapInterval == 0 && mSnapOffsets == null) { + smoothScrollAndSnap(velocityX); + return; + } + + int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth()); + int targetOffset = predictFinalScrollPosition(velocityX); + int smallerOffset = 0; + int largerOffset = maximumOffset; + int firstOffset = 0; + int lastOffset = maximumOffset; + int width = getWidth() - getPaddingStart() - getPaddingEnd(); // offsets are from the right edge in RTL layouts boolean isRTL = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 88db98ad2..8454527b0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -310,7 +310,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou @Override public void fling(int velocityY) { if (mPagingEnabled) { - smoothScrollAndSnap(velocityY); + flingAndSnap(velocityY); } else if (mScroller != null) { // FB SCROLLVIEW CHANGE @@ -433,7 +433,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou // Only if we have pagingEnabled and we have not snapped to the page do we // need to continue checking for the scroll. And we cause that scroll by asking for it mSnappingToPage = true; - smoothScrollAndSnap(0); + flingAndSnap(0); ViewCompat.postOnAnimationDelayed(ReactScrollView.this, this, ReactScrollViewHelper.MOMENTUM_DELAY); @@ -452,23 +452,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou ReactScrollViewHelper.MOMENTUM_DELAY); } - /** - * This will smooth scroll us to the nearest snap offset point - * It currently just looks at where the content is and slides to the nearest point. - * It is intended to be run after we are done scrolling, and handling any momentum scrolling. - */ - private void smoothScrollAndSnap(int velocityY) { - if (getChildCount() <= 0) { - return; - } - - int maximumOffset = getMaxScrollY(); - int targetOffset = 0; - int smallerOffset = 0; - int largerOffset = maximumOffset; - int firstOffset = 0; - int lastOffset = maximumOffset; - + private int predictFinalScrollPosition(int velocityY) { // ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's // no way to customize the scroll duration. So, we create a temporary OverScroller // so we can predict where a fling would land and snap to nearby that point. @@ -476,6 +460,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou scroller.setFriction(1.0f - mDecelerationRate); // predict where a fling would end up so we can scroll to the nearest snap offset + int maximumOffset = getMaxScrollY(); int height = getHeight() - getPaddingBottom() - getPaddingTop(); scroller.fling( getScrollX(), // startX @@ -489,7 +474,76 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou 0, // overX height/2 // overY ); - targetOffset = scroller.getFinalY(); + return scroller.getFinalY(); + } + + /** + * This will smooth scroll us to the nearest snap offset point + * It currently just looks at where the content is and slides to the nearest point. + * It is intended to be run after we are done scrolling, and handling any momentum scrolling. + */ + private void smoothScrollAndSnap(int velocity) { + double interval = (double) getSnapInterval(); + double currentOffset = (double) getScrollY(); + double targetOffset = (double) predictFinalScrollPosition(velocity); + + int previousPage = (int) Math.floor(currentOffset / interval); + int nextPage = (int) Math.ceil(currentOffset / interval); + int currentPage = (int) Math.round(currentOffset / interval); + int targetPage = (int) Math.round(targetOffset / interval); + + if (velocity > 0 && nextPage == previousPage) { + nextPage ++; + } else if (velocity < 0 && previousPage == nextPage) { + previousPage --; + } + + if ( + // if scrolling towards next page + velocity > 0 && + // and the middle of the page hasn't been crossed already + currentPage < nextPage && + // and it would have been crossed after flinging + targetPage > previousPage + ) { + currentPage = nextPage; + } + else if ( + // if scrolling towards previous page + velocity < 0 && + // and the middle of the page hasn't been crossed already + currentPage > previousPage && + // and it would have been crossed after flinging + targetPage < nextPage + ) { + currentPage = previousPage; + } + + targetOffset = currentPage * interval; + if (targetOffset != currentOffset) { + mActivelyScrolling = true; + smoothScrollTo(getScrollX(), (int) targetOffset); + } + } + + private void flingAndSnap(int velocityY) { + if (getChildCount() <= 0) { + return; + } + + // pagingEnabled only allows snapping one interval at a time + if (mSnapInterval == 0 && mSnapOffsets == null) { + smoothScrollAndSnap(velocityY); + return; + } + + int maximumOffset = getMaxScrollY(); + int targetOffset = predictFinalScrollPosition(velocityY); + int smallerOffset = 0; + int largerOffset = maximumOffset; + int firstOffset = 0; + int lastOffset = maximumOffset; + int height = getHeight() - getPaddingBottom() - getPaddingTop(); // get the nearest snap points to the target offset if (mSnapOffsets != null) {