Skip to content

Commit

Permalink
Fix ScrollView bounce back bug in open source
Browse files Browse the repository at this point in the history
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
astreet authored and Facebook Github Bot 7 committed Aug 1, 2016
1 parent 7cf4e36 commit 36ca1a0
Showing 1 changed file with 85 additions and 1 deletion.
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@


import javax.annotation.Nullable; import javax.annotation.Nullable;


import java.lang.reflect.Field;

import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.OverScroller;
import android.widget.ScrollView; import android.widget.ScrollView;


import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.MeasureSpecAssertions; import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.events.NativeGestureUtil; import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.view.ReactClippingViewGroup; import com.facebook.react.views.view.ReactClippingViewGroup;
Expand All @@ -36,7 +41,11 @@
*/ */
public class ReactScrollView extends ScrollView implements ReactClippingViewGroup { public class ReactScrollView extends ScrollView implements ReactClippingViewGroup {


private static Field sScrollerField;
private static boolean sTriedToGetScrollerField = false;

private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private final OverScroller mScroller;


private @Nullable Rect mClippingRect; private @Nullable Rect mClippingRect;
private boolean mDoneFlinging; private boolean mDoneFlinging;
Expand All @@ -57,6 +66,29 @@ public ReactScrollView(Context context) {
public ReactScrollView(Context context, @Nullable FpsListener fpsListener) { public ReactScrollView(Context context, @Nullable FpsListener fpsListener) {
super(context); super(context);
mFpsListener = fpsListener; 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) { public void setSendMomentumEvents(boolean sendMomentumEvents) {
Expand Down Expand Up @@ -187,7 +219,36 @@ public void getClippingRect(Rect outClippingRect) {


@Override @Override
public void fling(int velocityY) { 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()) { if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
mFlinging = true; mFlinging = true;
enableFpsListener(); enableFpsListener();
Expand Down Expand Up @@ -247,4 +308,27 @@ public void setEndFillColor(int color) {
mEndBackground = new ColorDrawable(mEndFillColor); 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.