Skip to content

Commit ff36fec

Browse files
olegblsjchmiela
authored andcommitted
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. facebook#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 facebook#21116. Reviewed By: shergin Differential Revision: D9945017 fbshipit-source-id: be7d4dfd1140f4c4d32bad93a03812dc80286069
1 parent d3b8284 commit ff36fec

File tree

2 files changed

+148
-36
lines changed

2 files changed

+148
-36
lines changed

ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java

+74-18
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public boolean onTouchEvent(MotionEvent ev) {
262262
@Override
263263
public void fling(int velocityX) {
264264
if (mPagingEnabled) {
265-
smoothScrollAndSnap(velocityX);
265+
flingAndSnap(velocityX);
266266
} else if (mScroller != null) {
267267
// FB SCROLLVIEW CHANGE
268268

@@ -440,7 +440,7 @@ public void run() {
440440
// Only if we have pagingEnabled and we have not snapped to the page do we
441441
// need to continue checking for the scroll. And we cause that scroll by asking for it
442442
mSnappingToPage = true;
443-
smoothScrollAndSnap(0);
443+
flingAndSnap(0);
444444
ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this,
445445
this,
446446
ReactScrollViewHelper.MOMENTUM_DELAY);
@@ -459,28 +459,15 @@ public void run() {
459459
ReactScrollViewHelper.MOMENTUM_DELAY);
460460
}
461461

462-
/**
463-
* This will smooth scroll us to the nearest snap offset point
464-
* It currently just looks at where the content is and slides to the nearest point.
465-
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
466-
*/
467-
private void smoothScrollAndSnap(int velocityX) {
468-
if (getChildCount() <= 0) {
469-
return;
470-
}
471-
472-
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
473-
int targetOffset = 0;
474-
int smallerOffset = 0;
475-
int largerOffset = maximumOffset;
476-
462+
private int predictFinalScrollPosition(int velocityX) {
477463
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
478464
// no way to customize the scroll duration. So, we create a temporary OverScroller
479465
// so we can predict where a fling would land and snap to nearby that point.
480466
OverScroller scroller = new OverScroller(getContext());
481467
scroller.setFriction(1.0f - mDecelerationRate);
482468

483469
// predict where a fling would end up so we can scroll to the nearest snap offset
470+
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
484471
int width = getWidth() - getPaddingStart() - getPaddingEnd();
485472
scroller.fling(
486473
getScrollX(), // startX
@@ -494,7 +481,76 @@ private void smoothScrollAndSnap(int velocityX) {
494481
width/2, // overX
495482
0 // overY
496483
);
497-
targetOffset = scroller.getFinalX();
484+
return scroller.getFinalX();
485+
}
486+
487+
/**
488+
* This will smooth scroll us to the nearest snap offset point
489+
* It currently just looks at where the content is and slides to the nearest point.
490+
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
491+
*/
492+
private void smoothScrollAndSnap(int velocity) {
493+
double interval = (double) getSnapInterval();
494+
double currentOffset = (double) getScrollX();
495+
double targetOffset = (double) predictFinalScrollPosition(velocity);
496+
497+
int previousPage = (int) Math.floor(currentOffset / interval);
498+
int nextPage = (int) Math.ceil(currentOffset / interval);
499+
int currentPage = (int) Math.round(currentOffset / interval);
500+
int targetPage = (int) Math.round(targetOffset / interval);
501+
502+
if (velocity > 0 && nextPage == previousPage) {
503+
nextPage ++;
504+
} else if (velocity < 0 && previousPage == nextPage) {
505+
previousPage --;
506+
}
507+
508+
if (
509+
// if scrolling towards next page
510+
velocity > 0 &&
511+
// and the middle of the page hasn't been crossed already
512+
currentPage < nextPage &&
513+
// and it would have been crossed after flinging
514+
targetPage > previousPage
515+
) {
516+
currentPage = nextPage;
517+
}
518+
else if (
519+
// if scrolling towards previous page
520+
velocity < 0 &&
521+
// and the middle of the page hasn't been crossed already
522+
currentPage > previousPage &&
523+
// and it would have been crossed after flinging
524+
targetPage < nextPage
525+
) {
526+
currentPage = previousPage;
527+
}
528+
529+
targetOffset = currentPage * interval;
530+
if (targetOffset != currentOffset) {
531+
mActivelyScrolling = true;
532+
smoothScrollTo((int) targetOffset, getScrollY());
533+
}
534+
}
535+
536+
private void flingAndSnap(int velocityX) {
537+
if (getChildCount() <= 0) {
538+
return;
539+
}
540+
541+
// pagingEnabled only allows snapping one interval at a time
542+
if (mSnapInterval == 0 && mSnapOffsets == null) {
543+
smoothScrollAndSnap(velocityX);
544+
return;
545+
}
546+
547+
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
548+
int targetOffset = predictFinalScrollPosition(velocityX);
549+
int smallerOffset = 0;
550+
int largerOffset = maximumOffset;
551+
int firstOffset = 0;
552+
int lastOffset = maximumOffset;
553+
int width = getWidth() - getPaddingStart() - getPaddingEnd();
498554

499555
// offsets are from the right edge in RTL layouts
500556
boolean isRTL = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;

ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java

+74-18
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ public void getClippingRect(Rect outClippingRect) {
293293
@Override
294294
public void fling(int velocityY) {
295295
if (mPagingEnabled) {
296-
smoothScrollAndSnap(velocityY);
296+
flingAndSnap(velocityY);
297297
} else if (mScroller != null) {
298298
// FB SCROLLVIEW CHANGE
299299

@@ -408,7 +408,7 @@ public void run() {
408408
// Only if we have pagingEnabled and we have not snapped to the page do we
409409
// need to continue checking for the scroll. And we cause that scroll by asking for it
410410
mSnappingToPage = true;
411-
smoothScrollAndSnap(0);
411+
flingAndSnap(0);
412412
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
413413
this,
414414
ReactScrollViewHelper.MOMENTUM_DELAY);
@@ -427,28 +427,15 @@ public void run() {
427427
ReactScrollViewHelper.MOMENTUM_DELAY);
428428
}
429429

430-
/**
431-
* This will smooth scroll us to the nearest snap offset point
432-
* It currently just looks at where the content is and slides to the nearest point.
433-
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
434-
*/
435-
private void smoothScrollAndSnap(int velocityY) {
436-
if (getChildCount() <= 0) {
437-
return;
438-
}
439-
440-
int maximumOffset = getMaxScrollY();
441-
int targetOffset = 0;
442-
int smallerOffset = 0;
443-
int largerOffset = maximumOffset;
444-
430+
private int predictFinalScrollPosition(int velocityY) {
445431
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
446432
// no way to customize the scroll duration. So, we create a temporary OverScroller
447433
// so we can predict where a fling would land and snap to nearby that point.
448434
OverScroller scroller = new OverScroller(getContext());
449435
scroller.setFriction(1.0f - mDecelerationRate);
450436

451437
// predict where a fling would end up so we can scroll to the nearest snap offset
438+
int maximumOffset = getMaxScrollY();
452439
int height = getHeight() - getPaddingBottom() - getPaddingTop();
453440
scroller.fling(
454441
getScrollX(), // startX
@@ -462,7 +449,76 @@ private void smoothScrollAndSnap(int velocityY) {
462449
0, // overX
463450
height/2 // overY
464451
);
465-
targetOffset = scroller.getFinalY();
452+
return scroller.getFinalY();
453+
}
454+
455+
/**
456+
* This will smooth scroll us to the nearest snap offset point
457+
* It currently just looks at where the content is and slides to the nearest point.
458+
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
459+
*/
460+
private void smoothScrollAndSnap(int velocity) {
461+
double interval = (double) getSnapInterval();
462+
double currentOffset = (double) getScrollY();
463+
double targetOffset = (double) predictFinalScrollPosition(velocity);
464+
465+
int previousPage = (int) Math.floor(currentOffset / interval);
466+
int nextPage = (int) Math.ceil(currentOffset / interval);
467+
int currentPage = (int) Math.round(currentOffset / interval);
468+
int targetPage = (int) Math.round(targetOffset / interval);
469+
470+
if (velocity > 0 && nextPage == previousPage) {
471+
nextPage ++;
472+
} else if (velocity < 0 && previousPage == nextPage) {
473+
previousPage --;
474+
}
475+
476+
if (
477+
// if scrolling towards next page
478+
velocity > 0 &&
479+
// and the middle of the page hasn't been crossed already
480+
currentPage < nextPage &&
481+
// and it would have been crossed after flinging
482+
targetPage > previousPage
483+
) {
484+
currentPage = nextPage;
485+
}
486+
else if (
487+
// if scrolling towards previous page
488+
velocity < 0 &&
489+
// and the middle of the page hasn't been crossed already
490+
currentPage > previousPage &&
491+
// and it would have been crossed after flinging
492+
targetPage < nextPage
493+
) {
494+
currentPage = previousPage;
495+
}
496+
497+
targetOffset = currentPage * interval;
498+
if (targetOffset != currentOffset) {
499+
mActivelyScrolling = true;
500+
smoothScrollTo(getScrollX(), (int) targetOffset);
501+
}
502+
}
503+
504+
private void flingAndSnap(int velocityY) {
505+
if (getChildCount() <= 0) {
506+
return;
507+
}
508+
509+
// pagingEnabled only allows snapping one interval at a time
510+
if (mSnapInterval == 0 && mSnapOffsets == null) {
511+
smoothScrollAndSnap(velocityY);
512+
return;
513+
}
514+
515+
int maximumOffset = getMaxScrollY();
516+
int targetOffset = predictFinalScrollPosition(velocityY);
517+
int smallerOffset = 0;
518+
int largerOffset = maximumOffset;
519+
int firstOffset = 0;
520+
int lastOffset = maximumOffset;
521+
int height = getHeight() - getPaddingBottom() - getPaddingTop();
466522

467523
// get the nearest snap points to the target offset
468524
if (mSnapOffsets != null) {

0 commit comments

Comments
 (0)