Permalink
Browse files

Fix ScrollView bounce back bug in open source

Summary: We now reach in and use the Scroller directly, reimplementing fling() and onOverScrolled(). I verified that in Android 4.1.2 ScrollView#mScroller exists as a private on ScrollView, but there's still potential that this could break things if OEMs have modified ScrollView so we just log a warning if we can't get access to that field.

Reviewed By: foghina

Differential Revision: D3650008

fbshipit-source-id: e52909bf9d6008f6d1ecd458aee25fe82ffaac19
  • Loading branch information...
1 parent 7cf4e36 commit 36ca1a078acbaba770c196847254e7a6e80c6918 @astreet astreet committed with Facebook Github Bot 7 Aug 1, 2016
Showing with 85 additions and 1 deletion.
  1. +85 −1 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
@@ -11,16 +11,21 @@
import javax.annotation.Nullable;
+import java.lang.reflect.Field;
+
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
+import android.widget.OverScroller;
import android.widget.ScrollView;
+import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.view.ReactClippingViewGroup;
@@ -36,7 +41,11 @@
*/
public class ReactScrollView extends ScrollView implements ReactClippingViewGroup {
+ private static Field sScrollerField;
+ private static boolean sTriedToGetScrollerField = false;
+
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
+ private final OverScroller mScroller;
private @Nullable Rect mClippingRect;
private boolean mDoneFlinging;
@@ -57,6 +66,29 @@ public ReactScrollView(Context context) {
public ReactScrollView(Context context, @Nullable FpsListener fpsListener) {
super(context);
mFpsListener = fpsListener;
+
+ if (!sTriedToGetScrollerField) {
+ sTriedToGetScrollerField = true;
+ try {
+ sScrollerField = ScrollView.class.getDeclaredField("mScroller");
+ sScrollerField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.w(
+ ReactConstants.TAG,
+ "Failed to get mScroller field for ScrollView! " +
+ "This app will exhibit the bounce-back scrolling bug :(");
+ }
+ }
+
+ if (sScrollerField != null) {
+ try {
+ mScroller = (OverScroller) sScrollerField.get(this);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
+ }
+ } else {
+ mScroller = null;
+ }
}
public void setSendMomentumEvents(boolean sendMomentumEvents) {
@@ -187,7 +219,36 @@ public void getClippingRect(Rect outClippingRect) {
@Override
public void fling(int velocityY) {
- super.fling(velocityY);
+ if (mScroller != null) {
+ // FB SCROLLVIEW CHANGE
+
+ // We provide our own version of fling that uses a different call to the standard OverScroller
+ // which takes into account the possibility of adding new content while the ScrollView is
+ // animating. Because we give essentially no max Y for the fling, the fling will continue as long
+ // as there is content. See #onOverScrolled() to see the second part of this change which properly
+ // aborts the scroller animation when we get to the bottom of the ScrollView content.
+
+ int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
+
+ mScroller.fling(
+ getScrollX(),
+ getScrollY(),
+ 0,
+ velocityY,
+ 0,
+ 0,
+ 0,
+ Integer.MAX_VALUE,
+ 0,
+ scrollWindowHeight / 2);
+
+ postInvalidateOnAnimation();
+
+ // END FB SCROLLVIEW CHANGE
+ } else {
+ super.fling(velocityY);
+ }
+
if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
mFlinging = true;
enableFpsListener();
@@ -247,4 +308,27 @@ public void setEndFillColor(int color) {
mEndBackground = new ColorDrawable(mEndFillColor);
}
}
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+ if (mScroller != null) {
+ // FB SCROLLVIEW CHANGE
+
+ // This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
+ // more information.
+
+ if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
+ int scrollRange = Math.max(
+ 0,
+ getChildAt(0).getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
+ if (scrollY >= scrollRange) {
+ mScroller.abortAnimation();
+ }
+ }
+
+ // END FB SCROLLVIEW CHANGE
+ }
+
+ super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
+ }
}

0 comments on commit 36ca1a0

Please sign in to comment.