in ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java [915:1109]
private void flingAndSnap(int velocityX) {
if (DEBUG_MODE) {
FLog.i(TAG, "smoothScrollAndSnap[%d] velocityX %d", getId(), velocityX);
}
if (getChildCount() <= 0) {
return;
}
// pagingEnabled only allows snapping one interval at a time
if (mSnapInterval == 0 && mSnapOffsets == null && mSnapToAlignment == SNAP_ALIGNMENT_DISABLED) {
smoothScrollAndSnap(velocityX);
return;
}
boolean hasCustomizedFlingAnimator = getFlingAnimator() != DEFAULT_FLING_ANIMATOR;
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
int targetOffset = predictFinalScrollPosition(velocityX);
if (mDisableIntervalMomentum) {
targetOffset = getScrollX();
}
int smallerOffset = 0;
int largerOffset = maximumOffset;
int firstOffset = 0;
int lastOffset = maximumOffset;
int width = getWidth() - ViewCompat.getPaddingStart(this) - ViewCompat.getPaddingEnd(this);
int layoutDirection = getReactScrollViewScrollState().getLayoutDirection();
// offsets are from the right edge in RTL layouts
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
targetOffset = maximumOffset - targetOffset;
velocityX = -velocityX;
}
// get the nearest snap points to the target offset
if (mSnapOffsets != null && !mSnapOffsets.isEmpty()) {
firstOffset = mSnapOffsets.get(0);
lastOffset = mSnapOffsets.get(mSnapOffsets.size() - 1);
for (int i = 0; i < mSnapOffsets.size(); i++) {
int offset = mSnapOffsets.get(i);
if (offset <= targetOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
smallerOffset = offset;
}
}
if (offset >= targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
largerOffset = offset;
}
}
}
} else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED) {
if (mSnapInterval > 0) {
double ratio = (double) targetOffset / mSnapInterval;
smallerOffset =
Math.max(
getItemStartOffset(
mSnapToAlignment,
(int) (Math.floor(ratio) * mSnapInterval),
mSnapInterval,
width),
0);
largerOffset =
Math.min(
getItemStartOffset(
mSnapToAlignment,
(int) (Math.ceil(ratio) * mSnapInterval),
mSnapInterval,
width),
maximumOffset);
} else {
ViewGroup contentView = (ViewGroup) getContentView();
int smallerChildOffset = largerOffset;
int largerChildOffset = smallerOffset;
for (int i = 0; i < contentView.getChildCount(); i++) {
View item = contentView.getChildAt(i);
int itemStartOffset =
getItemStartOffset(mSnapToAlignment, item.getLeft(), item.getWidth(), width);
if (itemStartOffset <= targetOffset) {
if (targetOffset - itemStartOffset < targetOffset - smallerOffset) {
smallerOffset = itemStartOffset;
}
}
if (itemStartOffset >= targetOffset) {
if (itemStartOffset - targetOffset < largerOffset - targetOffset) {
largerOffset = itemStartOffset;
}
}
smallerChildOffset = Math.min(smallerChildOffset, itemStartOffset);
largerChildOffset = Math.max(largerChildOffset, itemStartOffset);
}
// For Recycler ViewGroup, the maximumOffset can be much larger than the total heights of
// items in the layout. In this case snapping is not possible beyond the currently rendered
// children.
smallerOffset = Math.max(smallerOffset, smallerChildOffset);
largerOffset = Math.min(largerOffset, largerChildOffset);
}
} else {
double interval = getSnapInterval();
double ratio = (double) targetOffset / interval;
smallerOffset = (int) (Math.floor(ratio) * interval);
largerOffset = Math.min((int) (Math.ceil(ratio) * interval), maximumOffset);
}
// Calculate the nearest offset
int nearestOffset =
Math.abs(targetOffset - smallerOffset) < Math.abs(largerOffset - targetOffset)
? smallerOffset
: largerOffset;
// if scrolling after the last snap offset and snapping to the
// end of the list is disabled, then we allow free scrolling
int currentOffset = getScrollX();
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
currentOffset = maximumOffset - currentOffset;
}
if (!mSnapToEnd && targetOffset >= lastOffset) {
if (currentOffset >= lastOffset) {
// free scrolling
} else {
// snap to end
targetOffset = lastOffset;
}
} else if (!mSnapToStart && targetOffset <= firstOffset) {
if (currentOffset <= firstOffset) {
// free scrolling
} else {
// snap to beginning
targetOffset = firstOffset;
}
} else if (velocityX > 0) {
if (!hasCustomizedFlingAnimator) {
// The default animator requires boost on initial velocity as when snapping velocity can
// feel sluggish for slow swipes
velocityX += (int) ((largerOffset - targetOffset) * 10.0);
}
targetOffset = largerOffset;
} else if (velocityX < 0) {
if (!hasCustomizedFlingAnimator) {
// The default animator requires boost on initial velocity as when snapping velocity can
// feel sluggish for slow swipes
velocityX -= (int) ((targetOffset - smallerOffset) * 10.0);
}
targetOffset = smallerOffset;
} else {
targetOffset = nearestOffset;
}
// Make sure the new offset isn't out of bounds
targetOffset = Math.min(Math.max(0, targetOffset), maximumOffset);
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
targetOffset = maximumOffset - targetOffset;
velocityX = -velocityX;
}
if (hasCustomizedFlingAnimator || mScroller == null) {
reactSmoothScrollTo(targetOffset, getScrollY());
} else {
// smoothScrollTo will always scroll over 250ms which is often *waaay*
// too short and will cause the scrolling to feel almost instant
// try to manually interact with OverScroller instead
// if velocity is 0 however, fling() won't work, so we want to use smoothScrollTo
mActivelyScrolling = true;
mScroller.fling(
getScrollX(), // startX
getScrollY(), // startY
// velocity = 0 doesn't work with fling() so we pretend there's a reasonable
// initial velocity going on when a touch is released without any movement
velocityX != 0 ? velocityX : targetOffset - getScrollX(), // velocityX
0, // velocityY
// setting both minX and maxX to the same value will guarantee that we scroll to it
// but using the standard fling-style easing rather than smoothScrollTo's 250ms animation
targetOffset, // minX
targetOffset, // maxX
0, // minY
0, // maxY
// we only want to allow overscrolling if the final offset is at the very edge of the view
(targetOffset == 0 || targetOffset == maximumOffset) ? width / 2 : 0, // overX
0 // overY
);
postInvalidateOnAnimation();
}
}