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
This commit is contained in:
Andy Street 2016-08-01 11:52:55 -07:00 committed by Facebook Github Bot 7
parent 7cf4e3665a
commit 36ca1a078a

View File

@ -11,16 +11,21 @@ package com.facebook.react.views.scroll;
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 @@ import com.facebook.infer.annotation.Assertions;
*/
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 class ReactScrollView extends ScrollView implements ReactClippingViewGrou
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 class ReactScrollView extends ScrollView implements ReactClippingViewGrou
@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 class ReactScrollView extends ScrollView implements ReactClippingViewGrou
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);
}
}