Skip to content

Commit e0170a9

Browse files
olegblfacebook-github-bot
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. #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
1 parent 28dedfb commit e0170a9

File tree

2 files changed

+148
-40
lines changed

2 files changed

+148
-40
lines changed

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

+74-20
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public boolean onTouchEvent(MotionEvent ev) {
287287
@Override
288288
public void fling(int velocityX) {
289289
if (mPagingEnabled) {
290-
smoothScrollAndSnap(velocityX);
290+
flingAndSnap(velocityX);
291291
} else if (mScroller != null) {
292292
// FB SCROLLVIEW CHANGE
293293

@@ -465,7 +465,7 @@ public void run() {
465465
// Only if we have pagingEnabled and we have not snapped to the page do we
466466
// need to continue checking for the scroll. And we cause that scroll by asking for it
467467
mSnappingToPage = true;
468-
smoothScrollAndSnap(0);
468+
flingAndSnap(0);
469469
ViewCompat.postOnAnimationDelayed(ReactHorizontalScrollView.this,
470470
this,
471471
ReactScrollViewHelper.MOMENTUM_DELAY);
@@ -484,30 +484,15 @@ public void run() {
484484
ReactScrollViewHelper.MOMENTUM_DELAY);
485485
}
486486

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 velocityX) {
493-
if (getChildCount() <= 0) {
494-
return;
495-
}
496-
497-
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
498-
int targetOffset = 0;
499-
int smallerOffset = 0;
500-
int largerOffset = maximumOffset;
501-
int firstOffset = 0;
502-
int lastOffset = maximumOffset;
503-
487+
private int predictFinalScrollPosition(int velocityX) {
504488
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
505489
// no way to customize the scroll duration. So, we create a temporary OverScroller
506490
// so we can predict where a fling would land and snap to nearby that point.
507491
OverScroller scroller = new OverScroller(getContext());
508492
scroller.setFriction(1.0f - mDecelerationRate);
509493

510494
// predict where a fling would end up so we can scroll to the nearest snap offset
495+
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
511496
int width = getWidth() - getPaddingStart() - getPaddingEnd();
512497
scroller.fling(
513498
getScrollX(), // startX
@@ -521,7 +506,76 @@ private void smoothScrollAndSnap(int velocityX) {
521506
width/2, // overX
522507
0 // overY
523508
);
524-
targetOffset = scroller.getFinalX();
509+
return scroller.getFinalX();
510+
}
511+
512+
/**
513+
* This will smooth scroll us to the nearest snap offset point
514+
* It currently just looks at where the content is and slides to the nearest point.
515+
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
516+
*/
517+
private void smoothScrollAndSnap(int velocity) {
518+
double interval = (double) getSnapInterval();
519+
double currentOffset = (double) getScrollX();
520+
double targetOffset = (double) predictFinalScrollPosition(velocity);
521+
522+
int previousPage = (int) Math.floor(currentOffset / interval);
523+
int nextPage = (int) Math.ceil(currentOffset / interval);
524+
int currentPage = (int) Math.round(currentOffset / interval);
525+
int targetPage = (int) Math.round(targetOffset / interval);
526+
527+
if (velocity > 0 && nextPage == previousPage) {
528+
nextPage ++;
529+
} else if (velocity < 0 && previousPage == nextPage) {
530+
previousPage --;
531+
}
532+
533+
if (
534+
// if scrolling towards next page
535+
velocity > 0 &&
536+
// and the middle of the page hasn't been crossed already
537+
currentPage < nextPage &&
538+
// and it would have been crossed after flinging
539+
targetPage > previousPage
540+
) {
541+
currentPage = nextPage;
542+
}
543+
else if (
544+
// if scrolling towards previous page
545+
velocity < 0 &&
546+
// and the middle of the page hasn't been crossed already
547+
currentPage > previousPage &&
548+
// and it would have been crossed after flinging
549+
targetPage < nextPage
550+
) {
551+
currentPage = previousPage;
552+
}
553+
554+
targetOffset = currentPage * interval;
555+
if (targetOffset != currentOffset) {
556+
mActivelyScrolling = true;
557+
smoothScrollTo((int) targetOffset, getScrollY());
558+
}
559+
}
560+
561+
private void flingAndSnap(int velocityX) {
562+
if (getChildCount() <= 0) {
563+
return;
564+
}
565+
566+
// pagingEnabled only allows snapping one interval at a time
567+
if (mSnapInterval == 0 && mSnapOffsets == null) {
568+
smoothScrollAndSnap(velocityX);
569+
return;
570+
}
571+
572+
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
573+
int targetOffset = predictFinalScrollPosition(velocityX);
574+
int smallerOffset = 0;
575+
int largerOffset = maximumOffset;
576+
int firstOffset = 0;
577+
int lastOffset = maximumOffset;
578+
int width = getWidth() - getPaddingStart() - getPaddingEnd();
525579

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

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

+74-20
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ public void getClippingRect(Rect outClippingRect) {
310310
@Override
311311
public void fling(int velocityY) {
312312
if (mPagingEnabled) {
313-
smoothScrollAndSnap(velocityY);
313+
flingAndSnap(velocityY);
314314
} else if (mScroller != null) {
315315
// FB SCROLLVIEW CHANGE
316316

@@ -433,7 +433,7 @@ public void run() {
433433
// Only if we have pagingEnabled and we have not snapped to the page do we
434434
// need to continue checking for the scroll. And we cause that scroll by asking for it
435435
mSnappingToPage = true;
436-
smoothScrollAndSnap(0);
436+
flingAndSnap(0);
437437
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
438438
this,
439439
ReactScrollViewHelper.MOMENTUM_DELAY);
@@ -452,30 +452,15 @@ public void run() {
452452
ReactScrollViewHelper.MOMENTUM_DELAY);
453453
}
454454

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 velocityY) {
461-
if (getChildCount() <= 0) {
462-
return;
463-
}
464-
465-
int maximumOffset = getMaxScrollY();
466-
int targetOffset = 0;
467-
int smallerOffset = 0;
468-
int largerOffset = maximumOffset;
469-
int firstOffset = 0;
470-
int lastOffset = maximumOffset;
471-
455+
private int predictFinalScrollPosition(int velocityY) {
472456
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
473457
// no way to customize the scroll duration. So, we create a temporary OverScroller
474458
// so we can predict where a fling would land and snap to nearby that point.
475459
OverScroller scroller = new OverScroller(getContext());
476460
scroller.setFriction(1.0f - mDecelerationRate);
477461

478462
// predict where a fling would end up so we can scroll to the nearest snap offset
463+
int maximumOffset = getMaxScrollY();
479464
int height = getHeight() - getPaddingBottom() - getPaddingTop();
480465
scroller.fling(
481466
getScrollX(), // startX
@@ -489,7 +474,76 @@ private void smoothScrollAndSnap(int velocityY) {
489474
0, // overX
490475
height/2 // overY
491476
);
492-
targetOffset = scroller.getFinalY();
477+
return scroller.getFinalY();
478+
}
479+
480+
/**
481+
* This will smooth scroll us to the nearest snap offset point
482+
* It currently just looks at where the content is and slides to the nearest point.
483+
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
484+
*/
485+
private void smoothScrollAndSnap(int velocity) {
486+
double interval = (double) getSnapInterval();
487+
double currentOffset = (double) getScrollY();
488+
double targetOffset = (double) predictFinalScrollPosition(velocity);
489+
490+
int previousPage = (int) Math.floor(currentOffset / interval);
491+
int nextPage = (int) Math.ceil(currentOffset / interval);
492+
int currentPage = (int) Math.round(currentOffset / interval);
493+
int targetPage = (int) Math.round(targetOffset / interval);
494+
495+
if (velocity > 0 && nextPage == previousPage) {
496+
nextPage ++;
497+
} else if (velocity < 0 && previousPage == nextPage) {
498+
previousPage --;
499+
}
500+
501+
if (
502+
// if scrolling towards next page
503+
velocity > 0 &&
504+
// and the middle of the page hasn't been crossed already
505+
currentPage < nextPage &&
506+
// and it would have been crossed after flinging
507+
targetPage > previousPage
508+
) {
509+
currentPage = nextPage;
510+
}
511+
else if (
512+
// if scrolling towards previous page
513+
velocity < 0 &&
514+
// and the middle of the page hasn't been crossed already
515+
currentPage > previousPage &&
516+
// and it would have been crossed after flinging
517+
targetPage < nextPage
518+
) {
519+
currentPage = previousPage;
520+
}
521+
522+
targetOffset = currentPage * interval;
523+
if (targetOffset != currentOffset) {
524+
mActivelyScrolling = true;
525+
smoothScrollTo(getScrollX(), (int) targetOffset);
526+
}
527+
}
528+
529+
private void flingAndSnap(int velocityY) {
530+
if (getChildCount() <= 0) {
531+
return;
532+
}
533+
534+
// pagingEnabled only allows snapping one interval at a time
535+
if (mSnapInterval == 0 && mSnapOffsets == null) {
536+
smoothScrollAndSnap(velocityY);
537+
return;
538+
}
539+
540+
int maximumOffset = getMaxScrollY();
541+
int targetOffset = predictFinalScrollPosition(velocityY);
542+
int smallerOffset = 0;
543+
int largerOffset = maximumOffset;
544+
int firstOffset = 0;
545+
int lastOffset = maximumOffset;
546+
int height = getHeight() - getPaddingBottom() - getPaddingTop();
493547

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

0 commit comments

Comments
 (0)