Android ScrollView fix for pagingEnabled

Summary:
The snapToOffsets changes improved the flinging algorithm for snapToInterval/snapToOffsets but actually broke it for pagingEnabled because it's meant to only scroll one page at a time.

First, I just brough back the old algorithm, but noticed that it has a bunch of issues (e.g. #20155). So, I tried to improve the algorithm to make sure it uses the proper target offset prediction using the same physics model that Android uses for it's normal scrolling but still be limited to one page scrolls.

This resolves #21116.

Reviewed By: shergin

Differential Revision: D9945017

fbshipit-source-id: be7d4dfd1140f4c4d32bad93a03812dc80286069
This commit is contained in:
Oleg Lokhvitsky 2018-09-24 10:14:57 -07:00 committed by Facebook Github Bot
parent 28dedfb6d6
commit e0170a9445
2 changed files with 148 additions and 40 deletions

View File

@ -287,7 +287,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
@Override @Override
public void fling(int velocityX) { public void fling(int velocityX) {
if (mPagingEnabled) { if (mPagingEnabled) {
smoothScrollAndSnap(velocityX); flingAndSnap(velocityX);
} else if (mScroller != null) { } else if (mScroller != null) {
// FB SCROLLVIEW CHANGE // 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 // 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 // need to continue checking for the scroll. And we cause that scroll by asking for it
mSnappingToPage = true; mSnappingToPage = true;
smoothScrollAndSnap(0); flingAndSnap(0);
ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this, ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this,
this, this,
ReactScrollViewHelper.MOMENTUM_DELAY); ReactScrollViewHelper.MOMENTUM_DELAY);
@ -484,23 +484,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
ReactScrollViewHelper.MOMENTUM_DELAY); ReactScrollViewHelper.MOMENTUM_DELAY);
} }
/** private int predictFinalScrollPosition(int velocityX) {
* 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;
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's // 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 // 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. // 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); scroller.setFriction(1.0f - mDecelerationRate);
// predict where a fling would end up so we can scroll to the nearest snap offset // 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(); int width = getWidth() - getPaddingStart() - getPaddingEnd();
scroller.fling( scroller.fling(
getScrollX(), // startX getScrollX(), // startX
@ -521,7 +506,76 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements
width/2, // overX width/2, // overX
0 // overY 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 // offsets are from the right edge in RTL layouts
boolean isRTL = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL; boolean isRTL = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;

View File

@ -310,7 +310,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
@Override @Override
public void fling(int velocityY) { public void fling(int velocityY) {
if (mPagingEnabled) { if (mPagingEnabled) {
smoothScrollAndSnap(velocityY); flingAndSnap(velocityY);
} else if (mScroller != null) { } else if (mScroller != null) {
// FB SCROLLVIEW CHANGE // 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 // 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 // need to continue checking for the scroll. And we cause that scroll by asking for it
mSnappingToPage = true; mSnappingToPage = true;
smoothScrollAndSnap(0); flingAndSnap(0);
ViewCompat.postOnAnimationDelayed(ReactScrollView.this, ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
this, this,
ReactScrollViewHelper.MOMENTUM_DELAY); ReactScrollViewHelper.MOMENTUM_DELAY);
@ -452,23 +452,7 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
ReactScrollViewHelper.MOMENTUM_DELAY); ReactScrollViewHelper.MOMENTUM_DELAY);
} }
/** private int predictFinalScrollPosition(int velocityY) {
* 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;
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's // 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 // 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. // 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); scroller.setFriction(1.0f - mDecelerationRate);
// predict where a fling would end up so we can scroll to the nearest snap offset // predict where a fling would end up so we can scroll to the nearest snap offset
int maximumOffset = getMaxScrollY();
int height = getHeight() - getPaddingBottom() - getPaddingTop(); int height = getHeight() - getPaddingBottom() - getPaddingTop();
scroller.fling( scroller.fling(
getScrollX(), // startX getScrollX(), // startX
@ -489,7 +474,76 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
0, // overX 0, // overX
height/2 // overY 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 // get the nearest snap points to the target offset
if (mSnapOffsets != null) { if (mSnapOffsets != null) {