Untitled diff

Creado El diff nunca expira
3 eliminaciones
712 líneas
4 adiciones
712 líneas
/**
/**
* Copyright (c) 2015-present, Facebook, Inc.
* Copyright (c) 2015-present, Facebook, Inc.
*
*
* This source code is licensed under the MIT license found in the
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* LICENSE file in the root directory of this source tree.
*/
*/


package com.facebook.react.views.scroll;
package com.facebook.react.views.scroll;


import android.annotation.TargetApi;
import android.annotation.TargetApi;
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.support.v4.widget.NestedScrollView;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewCompat;
import android.util.Log;
import android.util.Log;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup;
import android.widget.OverScroller;
import android.widget.OverScroller;
import android.widget.ScrollView;
import android.widget.ScrollView;


import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.views.view.ReactViewBackgroundManager;
import com.facebook.react.views.view.ReactViewBackgroundManager;


import java.lang.reflect.Field;
import java.lang.reflect.Field;
import java.util.List;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.Nullable;


/**
/**
* A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has
* A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has
* a scroll listener to send scroll events to JS.
* a scroll listener to send scroll events to JS.
*
*
* <p>ReactScrollView only supports vertical scrolling. For horizontal scrolling,
* <p>ReactScrollView only supports vertical scrolling. For horizontal scrolling,
* use {@link ReactHorizontalScrollView}.
* use {@link ReactHorizontalScrollView}.
*/
*/
@TargetApi(11)
@TargetApi(11)
public class ReactScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener {
public class ReactScrollView extends NestedScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener {


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


private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private final @Nullable OverScroller mScroller;
private final @Nullable OverScroller mScroller;
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final VelocityHelper mVelocityHelper = new VelocityHelper();
private final Rect mRect = new Rect(); // for reuse to avoid allocation
private final Rect mRect = new Rect(); // for reuse to avoid allocation


private boolean mActivelyScrolling;
private boolean mActivelyScrolling;
private @Nullable Rect mClippingRect;
private @Nullable Rect mClippingRect;
private @Nullable String mOverflow = ViewProps.HIDDEN;
private @Nullable String mOverflow = ViewProps.HIDDEN;
private boolean mDragging;
private boolean mDragging;
private boolean mPagingEnabled = false;
private boolean mPagingEnabled = false;
private @Nullable Runnable mPostTouchRunnable;
private @Nullable Runnable mPostTouchRunnable;
private boolean mRemoveClippedSubviews;
private boolean mRemoveClippedSubviews;
private boolean mScrollEnabled = true;
private boolean mScrollEnabled = true;
private boolean mSendMomentumEvents;
private boolean mSendMomentumEvents;
private @Nullable FpsListener mFpsListener = null;
private @Nullable FpsListener mFpsListener = null;
private @Nullable String mScrollPerfTag;
private @Nullable String mScrollPerfTag;
private @Nullable Drawable mEndBackground;
private @Nullable Drawable mEndBackground;
private int mEndFillColor = Color.TRANSPARENT;
private int mEndFillColor = Color.TRANSPARENT;
private int mSnapInterval = 0;
private int mSnapInterval = 0;
private float mDecelerationRate = 0.985f;
private float mDecelerationRate = 0.985f;
private @Nullable List<Integer> mSnapOffsets;
private @Nullable List<Integer> mSnapOffsets;
private View mContentView;
private View mContentView;
private ReactViewBackgroundManager mReactBackgroundManager;
private ReactViewBackgroundManager mReactBackgroundManager;


public ReactScrollView(ReactContext context) {
public ReactScrollView(ReactContext context) {
this(context, null);
this(context, null);
}
}


public ReactScrollView(ReactContext context, @Nullable FpsListener fpsListener) {
public ReactScrollView(ReactContext context, @Nullable FpsListener fpsListener) {
super(context);
super(context);
mFpsListener = fpsListener;
mFpsListener = fpsListener;
mReactBackgroundManager = new ReactViewBackgroundManager(this);
mReactBackgroundManager = new ReactViewBackgroundManager(this);


mScroller = getOverScrollerFromParent();
mScroller = getOverScrollerFromParent();
setOnHierarchyChangeListener(this);
setOnHierarchyChangeListener(this);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
}
}


@Nullable
@Nullable
private OverScroller getOverScrollerFromParent() {
private OverScroller getOverScrollerFromParent() {
OverScroller scroller;
OverScroller scroller;


if (!sTriedToGetScrollerField) {
if (!sTriedToGetScrollerField) {
sTriedToGetScrollerField = true;
sTriedToGetScrollerField = true;
try {
try {
sScrollerField = ScrollView.class.getDeclaredField("mScroller");
sScrollerField = ScrollView.class.getDeclaredField("mScroller");
sScrollerField.setAccessible(true);
sScrollerField.setAccessible(true);
} catch (NoSuchFieldException e) {
} catch (NoSuchFieldException e) {
Log.w(
Log.w(
ReactConstants.TAG,
ReactConstants.TAG,
"Failed to get mScroller field for ScrollView! " +
"Failed to get mScroller field for ScrollView! " +
"This app will exhibit the bounce-back scrolling bug :(");
"This app will exhibit the bounce-back scrolling bug :(");
}
}
}
}


if (sScrollerField != null) {
if (sScrollerField != null) {
try {
try {
Object scrollerValue = sScrollerField.get(this);
Object scrollerValue = sScrollerField.get(this);
if (scrollerValue instanceof OverScroller) {
if (scrollerValue instanceof OverScroller) {
scroller = (OverScroller) scrollerValue;
scroller = (OverScroller) scrollerValue;
} else {
} else {
Log.w(
Log.w(
ReactConstants.TAG,
ReactConstants.TAG,
"Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " +
"Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " +
"This app will exhibit the bounce-back scrolling bug :(");
"This app will exhibit the bounce-back scrolling bug :(");
scroller = null;
scroller = null;
}
}
} catch (IllegalAccessException e) {
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
}
}
} else {
} else {
scroller = null;
scroller = null;
}
}


return scroller;
return scroller;
}
}


public void setSendMomentumEvents(boolean sendMomentumEvents) {
public void setSendMomentumEvents(boolean sendMomentumEvents) {
mSendMomentumEvents = sendMomentumEvents;
mSendMomentumEvents = sendMomentumEvents;
}
}


public void setScrollPerfTag(@Nullable String scrollPerfTag) {
public void setScrollPerfTag(@Nullable String scrollPerfTag) {
mScrollPerfTag = scrollPerfTag;
mScrollPerfTag = scrollPerfTag;
}
}


public void setScrollEnabled(boolean scrollEnabled) {
public void setScrollEnabled(boolean scrollEnabled) {
mScrollEnabled = scrollEnabled;
mScrollEnabled = scrollEnabled;
}
}


public void setPagingEnabled(boolean pagingEnabled) {
public void setPagingEnabled(boolean pagingEnabled) {
mPagingEnabled = pagingEnabled;
mPagingEnabled = pagingEnabled;
}
}


public void setDecelerationRate(float decelerationRate) {
public void setDecelerationRate(float decelerationRate) {
mDecelerationRate = decelerationRate;
mDecelerationRate = decelerationRate;


if (mScroller != null) {
if (mScroller != null) {
mScroller.setFriction(1.0f - mDecelerationRate);
mScroller.setFriction(1.0f - mDecelerationRate);
}
}
}
}


public void setSnapInterval(int snapInterval) {
public void setSnapInterval(int snapInterval) {
mSnapInterval = snapInterval;
mSnapInterval = snapInterval;
}
}


public void setSnapOffsets(List<Integer> snapOffsets) {
public void setSnapOffsets(List<Integer> snapOffsets) {
mSnapOffsets = snapOffsets;
mSnapOffsets = snapOffsets;
}
}


public void flashScrollIndicators() {
public void flashScrollIndicators() {
awakenScrollBars();
awakenScrollBars();
}
}


public void setOverflow(String overflow) {
public void setOverflow(String overflow) {
mOverflow = overflow;
mOverflow = overflow;
invalidate();
invalidate();
}
}


@Override
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);


setMeasuredDimension(
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
MeasureSpec.getSize(heightMeasureSpec));
}
}


@Override
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Call with the present values in order to re-layout if necessary
// Call with the present values in order to re-layout if necessary
scrollTo(getScrollX(), getScrollY());
scrollTo(getScrollX(), getScrollY());
}
}


@Override
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
super.onSizeChanged(w, h, oldw, oldh);
if (mRemoveClippedSubviews) {
if (mRemoveClippedSubviews) {
updateClippingRect();
updateClippingRect();
}
}
}
}


@Override
@Override
protected void onAttachedToWindow() {
public void onAttachedToWindow() {
super.onAttachedToWindow();
super.onAttachedToWindow();
if (mRemoveClippedSubviews) {
if (mRemoveClippedSubviews) {
updateClippingRect();
updateClippingRect();
}
}
}
}


@Override
@Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
super.onScrollChanged(x, y, oldX, oldY);


mActivelyScrolling = true;
mActivelyScrolling = true;


if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
if (mRemoveClippedSubviews) {
if (mRemoveClippedSubviews) {
updateClippingRect();
updateClippingRect();
}
}


ReactScrollViewHelper.emitScrollEvent(
ReactScrollViewHelper.emitScrollEvent(
this,
this,
mOnScrollDispatchHelper.getXFlingVelocity(),
mOnScrollDispatchHelper.getXFlingVelocity(),
mOnScrollDispatchHelper.getYFlingVelocity());
mOnScrollDispatchHelper.getYFlingVelocity());
}
}
}
}


@Override
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mScrollEnabled) {
if (!mScrollEnabled) {
return false;
return false;
}
}


try {
try {
if (super.onInterceptTouchEvent(ev)) {
if (super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
mDragging = true;
mDragging = true;
enableFpsListener();
enableFpsListener();
return true;
return true;
}
}
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException e) {
// Log and ignore the error. This seems to be a bug in the android SDK and
// Log and ignore the error. This seems to be a bug in the android SDK and
// this is the commonly accepted workaround.
// this is the commonly accepted workaround.
// https://tinyurl.com/mw6qkod (Stack Overflow)
// https://tinyurl.com/mw6qkod (Stack Overflow)
Log.w(ReactConstants.TAG, "Error intercepting touch event.", e);
Log.w(ReactConstants.TAG, "Error intercepting touch event.", e);
}
}


return false;
return false;
}
}


@Override
@Override
public boolean onTouchEvent(MotionEvent ev) {
public boolean onTouchEvent(MotionEvent ev) {
if (!mScrollEnabled) {
if (!mScrollEnabled) {
return false;
return false;
}
}


mVelocityHelper.calculateVelocity(ev);
mVelocityHelper.calculateVelocity(ev);
int action = ev.getAction() & MotionEvent.ACTION_MASK;
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
if (action == MotionEvent.ACTION_UP && mDragging) {
float velocityX = mVelocityHelper.getXVelocity();
float velocityX = mVelocityHelper.getXVelocity();
float velocityY = mVelocityHelper.getYVelocity();
float velocityY = mVelocityHelper.getYVelocity();
ReactScrollViewHelper.emitScrollEndDragEvent(
ReactScrollViewHelper.emitScrollEndDragEvent(
this,
this,
velocityX,
velocityX,
velocityY);
velocityY);
mDragging = false;
mDragging = false;
// After the touch finishes, we may need to do some scrolling afterwards either as a result
// After the touch finishes, we may need to do some scrolling afterwards either as a result
// of a fling or because we need to page align the content
// of a fling or because we need to page align the content
handlePostTouchScrolling(Math.round(velocityX), Math.round(velocityY));
handlePostTouchScrolling(Math.round(velocityX), Math.round(velocityY));
}
}


return super.onTouchEvent(ev);
return super.onTouchEvent(ev);
}
}


@Override
@Override
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
if (removeClippedSubviews && mClippingRect == null) {
if (removeClippedSubviews && mClippingRect == null) {
mClippingRect = new Rect();
mClippingRect = new Rect();
}
}
mRemoveClippedSubviews = removeClippedSubviews;
mRemoveClippedSubviews = removeClippedSubviews;
updateClippingRect();
updateClippingRect();
}
}


@Override
@Override
public boolean getRemoveClippedSubviews() {
public boolean getRemoveClippedSubviews() {
return mRemoveClippedSubviews;
return mRemoveClippedSubviews;
}
}


@Override
@Override
public void updateClippingRect() {
public void updateClippingRect() {
if (!mRemoveClippedSubviews) {
if (!mRemoveClippedSubviews) {
return;
return;
}
}


Assertions.assertNotNull(mClippingRect);
Assertions.assertNotNull(mClippingRect);


ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
View contentView = getChildAt(0);
View contentView = getChildAt(0);
if (contentView instanceof ReactClippingViewGroup) {
if (contentView instanceof ReactClippingViewGroup) {
((ReactClippingViewGroup) contentView).updateClippingRect();
((ReactClippingViewGroup) contentView).updateClippingRect();
}
}
}
}


@Override
@Override
public void getClippingRect(Rect outClippingRect) {
public void getClippingRect(Rect outClippingRect) {
outClippingRect.set(Assertions.assertNotNull(mClippingRect));
outClippingRect.set(Assertions.assertNotNull(mClippingRect));
}
}


@Override
@Override
public void fling(int velocityY) {
public void fling(int velocityY) {
// Workaround.
// Workaround.
// On Android P if a ScrollView is inverted, we will get a wrong sign for
// On Android P if a ScrollView is inverted, we will get a wrong sign for
// velocityY (see https://issuetracker.google.com/issues/112385925).
// velocityY (see https://issuetracker.google.com/issues/112385925).
// At the same time, mOnScrollDispatchHelper tracks the correct velocity direction.
// At the same time, mOnScrollDispatchHelper tracks the correct velocity direction.
//
//
// Hence, we can use the absolute value from whatever the OS gives
// Hence, we can use the absolute value from whatever the OS gives
// us and use the sign of what mOnScrollDispatchHelper has tracked.
// us and use the sign of what mOnScrollDispatchHelper has tracked.
final int correctedVelocityY = (int)(Math.abs(velocityY) * Math.signum(mOnScrollDispatchHelper.getYFlingVelocity()));
final int correctedVelocityY = (int)(Math.abs(velocityY) * Math.signum(mOnScrollDispatchHelper.getYFlingVelocity()));



if (mPagingEnabled) {
if (mPagingEnabled) {
flingAndSnap(correctedVelocityY);
flingAndSnap(correctedVelocityY);
} else if (mScroller != null) {
} else if (mScroller != null) {
// FB SCROLLVIEW CHANGE
// FB SCROLLVIEW CHANGE


// We provide our own version of fling that uses a different call to the standard OverScroller
// 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
// 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
// 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
// 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.
// aborts the scroller animation when we get to the bottom of the ScrollView content.


int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();

mScroller.fling(
mScroller.fling(
getScrollX(), // startX
getScrollX(), // startX
getScrollY(), // startY
getScrollY(), // startY
0, // velocityX
0, // velocityX
correctedVelocityY, // velocityY
correctedVelocityY, // velocityY
0, // minX
0, // minX
0, // maxX
0, // maxX
0, // minY
0, // minY
Integer.MAX_VALUE, // maxY
Integer.MAX_VALUE, // maxY
0, // overX
0, // overX
scrollWindowHeight / 2 // overY
scrollWindowHeight / 2 // overY
);
);


ViewCompat.postInvalidateOnAnimation(this);
ViewCompat.postInvalidateOnAnimation(this);


// END FB SCROLLVIEW CHANGE
// END FB SCROLLVIEW CHANGE
} else {
} else {
super.fling(correctedVelocityY);
super.fling(correctedVelocityY);
}
}
handlePostTouchScrolling(0, correctedVelocityY);
handlePostTouchScrolling(0, correctedVelocityY);
}
}


private void enableFpsListener() {
private void enableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
if (isScrollPerfLoggingEnabled()) {
Assertions.assertNotNull(mFpsListener);
Assertions.assertNotNull(mFpsListener);
Assertions.assertNotNull(mScrollPerfTag);
Assertions.assertNotNull(mScrollPerfTag);
mFpsListener.enable(mScrollPerfTag);
mFpsListener.enable(mScrollPerfTag);
}
}
}
}


private void disableFpsListener() {
private void disableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
if (isScrollPerfLoggingEnabled()) {
Assertions.assertNotNull(mFpsListener);
Assertions.assertNotNull(mFpsListener);
Assertions.assertNotNull(mScrollPerfTag);
Assertions.assertNotNull(mScrollPerfTag);
mFpsListener.disable(mScrollPerfTag);
mFpsListener.disable(mScrollPerfTag);
}
}
}
}


private boolean isScrollPerfLoggingEnabled() {
private boolean isScrollPerfLoggingEnabled() {
return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty();
return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty();
}
}


private int getMaxScrollY() {
private int getMaxScrollY() {
int contentHeight = mContentView.getHeight();
int contentHeight = mContentView.getHeight();
int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop();
int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop();
return Math.max(0, contentHeight - viewportHeight);
return Math.max(0, contentHeight - viewportHeight);
}
}


@Override
@Override
public void draw(Canvas canvas) {
public void draw(Canvas canvas) {
if (mEndFillColor != Color.TRANSPARENT) {
if (mEndFillColor != Color.TRANSPARENT) {
final View content = getChildAt(0);
final View content = getChildAt(0);
if (mEndBackground != null && content != null && content.getBottom() < getHeight()) {
if (mEndBackground != null && content != null && content.getBottom() < getHeight()) {
mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight());
mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight());
mEndBackground.draw(canvas);
mEndBackground.draw(canvas);
}
}
}
}
getDrawingRect(mRect);
getDrawingRect(mRect);


switch (mOverflow) {
switch (mOverflow) {
case ViewProps.VISIBLE:
case ViewProps.VISIBLE:
break;
break;
default:
default:
canvas.clipRect(mRect);
canvas.clipRect(mRect);
break;
break;
}
}


super.draw(canvas);
super.draw(canvas);
}
}


/**
/**
* This handles any sort of scrolling that may occur after a touch is finished. This may be
* This handles any sort of scrolling that may occur after a touch is finished. This may be
* momentum scrolling (fling) or because you have pagingEnabled on the scroll view. Because we
* momentum scrolling (fling) or because you have pagingEnabled on the scroll view. Because we
* don't get any events from Android about this lifecycle, we do all our detection by creating a
* don't get any events from Android about this lifecycle, we do all our detection by creating a
* runnable that checks if we scrolled in the last frame and if so assumes we are still scrolling.
* runnable that checks if we scrolled in the last frame and if so assumes we are still scrolling.
*/
*/
private void handlePostTouchScrolling(int velocityX, int velocityY) {
private void handlePostTouchScrolling(int velocityX, int velocityY) {
// If we aren't going to do anything (send events or snap to page), we can early exit out.
// If we aren't going to do anything (send events or snap to page), we can early exit out.
if (!mSendMomentumEvents && !mPagingEnabled && !isScrollPerfLoggingEnabled()) {
if (!mSendMomentumEvents && !mPagingEnabled && !isScrollPerfLoggingEnabled()) {
return;
return;
}
}


// Check if we are already handling this which may occur if this is called by both the touch up
// Check if we are already handling this which may occur if this is called by both the touch up
// and a fling call
// and a fling call
if (mPostTouchRunnable != null) {
if (mPostTouchRunnable != null) {
return;
return;
}
}


if (mSendMomentumEvents) {
if (mSendMomentumEvents) {
enableFpsListener();
enableFpsListener();
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, velocityX, velocityY);
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, velocityX, velocityY);
}
}


mActivelyScrolling = false;
mActivelyScrolling = false;
mPostTouchRunnable = new Runnable() {
mPostTouchRunnable = new Runnable() {


private boolean mSnappingToPage = false;
private boolean mSnappingToPage = false;


@Override
@Override
public void run() {
public void run() {
if (mActivelyScrolling) {
if (mActivelyScrolling) {
// We are still scrolling so we just post to check again a frame later
// We are still scrolling so we just post to check again a frame later
mActivelyScrolling = false;
mActivelyScrolling = false;
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
this,
this,
ReactScrollViewHelper.MOMENTUM_DELAY);
ReactScrollViewHelper.MOMENTUM_DELAY);
} else {
} else {
if (mPagingEnabled && !mSnappingToPage) {
if (mPagingEnabled && !mSnappingToPage) {
// Only if we have pagingEnabled and we have not snapped to the page do we
// Only if we have pagingEnabled and we have not snapped to the page do we
// need to continue checking for the scroll. And we cause that scroll by asking for it
// need to continue checking for the scroll. And we cause that scroll by asking for it
mSnappingToPage = true;
mSnappingToPage = true;
flingAndSnap(0);
flingAndSnap(0);
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
this,
this,
ReactScrollViewHelper.MOMENTUM_DELAY);
ReactScrollViewHelper.MOMENTUM_DELAY);
} else {
} else {
if (mSendMomentumEvents) {
if (mSendMomentumEvents) {
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactScrollView.this);
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactScrollView.this);
}
}
ReactScrollView.this.mPostTouchRunnable = null;
ReactScrollView.this.mPostTouchRunnable = null;
disableFpsListener();
disableFpsListener();
}
}
}
}
}
}
};
};
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
ViewCompat.postOnAnimationDelayed(ReactScrollView.this,
mPostTouchRunnable,
mPostTouchRunnable,
ReactScrollViewHelper.MOMENTUM_DELAY);
ReactScrollViewHelper.MOMENTUM_DELAY);
}
}


private int predictFinalScrollPosition(int velocityY) {
private int predictFinalScrollPosition(int velocityY) {
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
// no way to customize the scroll duration. So, we create a temporary OverScroller
// no way to customize the scroll duration. So, we create a temporary OverScroller
// so we can predict where a fling would land and snap to nearby that point.
// so we can predict where a fling would land and snap to nearby that point.
OverScroller scroller = new OverScroller(getContext());
OverScroller scroller = new OverScroller(getContext());
scroller.setFriction(1.0f - mDecelerationRate);
scroller.setFriction(1.0f - mDecelerationRate);


// predict where a fling would end up so we can scroll to the nearest snap offset
// predict where a fling would end up so we can scroll to the nearest snap offset
int maximumOffset = getMaxScrollY();
int maximumOffset = getMaxScrollY();
int height = getHeight() - getPaddingBottom() - getPaddingTop();
int height = getHeight() - getPaddingBottom() - getPaddingTop();
scroller.fling(
scroller.fling(
getScrollX(), // startX
getScrollX(), // startX
getScrollY(), // startY
getScrollY(), // startY
0, // velocityX
0, // velocityX
velocityY, // velocityY
velocityY, // velocityY
0, // minX
0, // minX
0, // maxX
0, // maxX
0, // minY
0, // minY
maximumOffset, // maxY
maximumOffset, // maxY
0, // overX
0, // overX
height/2 // overY
height/2 // overY
);
);
return scroller.getFinalY();
return scroller.getFinalY();
}
}


/**
/**
* This will smooth scroll us to the nearest snap offset point
* This will smooth scroll us to the nearest snap offset point
* It currently just looks at where the content is and slides to the nearest point.
* It currently just looks at where the content is and slides to the nearest point.
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
* It is intended to be run after we are done scrolling, and handling any momentum scrolling.
*/
*/
private void smoothScrollAndSnap(int velocity) {
private void smoothScrollAndSnap(int velocity) {
double interval = (double) getSnapInterval();
double interval = (double) getSnapInterval();
double currentOffset = (double) getScrollY();
double currentOffset = (double) getScrollY();
double targetOffset = (double) predictFinalScrollPosition(velocity);
double targetOffset = (double) predictFinalScrollPosition(velocity);


int previousPage = (int) Math.floor(currentOffset / interval);
int previousPage = (int) Math.floor(currentOffset / interval);
int nextPage = (int) Math.ceil(currentOffset / interval);
int nextPage = (int) Math.ceil(currentOffset / interval);
int currentPage = (int) Math.round(currentOffset / interval);
int currentPage = (int) Math.round(currentOffset / interval);
int targetPage = (int) Math.round(targetOffset / interval);
int targetPage = (int) Math.round(targetOffset / interval);


if (velocity > 0 && nextPage == previousPage) {
if (velocity > 0 && nextPage == previousPage) {
nextPage ++;
nextPage ++;
} else if (velocity < 0 && previousPage == nextPage) {
} else if (velocity < 0 && previousPage == nextPage) {
previousPage --;
previousPage --;
}
}


if (
if (
// if scrolling towards next page
// if scrolling towards next page
velocity > 0 &&
velocity > 0 &&
// and the middle of the page hasn't been crossed already
// and the middle of the page hasn't been crossed already
currentPage < nextPage &&
currentPage < nextPage &&
// and it would have been crossed after flinging
// and it would have been crossed after flinging
targetPage > previousPage
targetPage > previousPage
) {
) {
currentPage = nextPage;
currentPage = nextPage;
}
}
else if (
else if (
// if scrolling towards previous page
// if scrolling towards previous page
velocity < 0 &&
velocity < 0 &&
// and the middle of the page hasn't been crossed already
// and the middle of the page hasn't been crossed already
currentPage > previousPage &&
currentPage > previousPage &&
// and it would have been crossed after flinging
// and it would have been crossed after flinging
targetPage < nextPage
targetPage < nextPage
) {
) {
currentPage = previousPage;
currentPage = previousPage;
}
}


targetOffset = currentPage * interval;
targetOffset = currentPage * interval;
if (targetOffset != currentOffset) {
if (targetOffset != currentOffset) {
mActivelyScrolling = true;
mActivelyScrolling = true;
smoothScrollTo(getScrollX(), (int) targetOffset);
smoothScrollTo(getScrollX(), (int) targetOffset);
}
}
}
}


private void flingAndSnap(int velocityY) {
private void flingAndSnap(int velocityY) {
if (getChildCount() <= 0) {
if (getChildCount() <= 0) {
return;
return;
}
}


// pagingEnabled only allows snapping one interval at a time
// pagingEnabled only allows snapping one interval at a time
if (mSnapInterval == 0 && mSnapOffsets == null) {
if (mSnapInterval == 0 && mSnapOffsets == null) {
smoothScrollAndSnap(velocityY);
smoothScrollAndSnap(velocityY);
return;
return;
}
}


int maximumOffset = getMaxScrollY();
int maximumOffset = getMaxScrollY();
int targetOffset = predictFinalScrollPosition(velocityY);
int targetOffset = predictFinalScrollPosition(velocityY);
int smallerOffset = 0;
int smallerOffset = 0;
int largerOffset = maximumOffset;
int largerOffset = maximumOffset;
int firstOffset = 0;
int firstOffset = 0;
int lastOffset = maximumOffset;
int lastOffset = maximumOffset;
int height = getHeight() - getPaddingBottom() - getPaddingTop();
int height = getHeight() - getPaddingBottom() - getPaddingTop();


// get the nearest snap points to the target offset
// get the nearest snap points to the target offset
if (mSnapOffsets != null) {
if (mSnapOffsets != null) {
for (int i = 0; i < mSnapOffsets.size(); i ++) {
for (int i = 0; i < mSnapOffsets.size(); i ++) {
int offset = mSnapOffsets.get(i);
int offset = mSnapOffsets.get(i);


if (offset <= targetOffset) {
if (offset <= targetOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
smallerOffset = offset;
smallerOffset = offset;
}
}
}
}


if (offset >= targetOffset) {
if (offset >= targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
largerOffset = offset;
largerOffset = offset;
}
}
}
}
}
}
} else {
} else {
double interval = (double) getSnapInterval();
double interval = (double) getSnapInterval();
double ratio = (double) targetOffset / interval;
double ratio = (double) targetOffset / interval;
smallerOffset = (int) (Math.floor(ratio) * interval);
smallerOffset = (int) (Math.floor(ratio) * interval);
largerOffset = Math.min((int) (Math.ceil(ratio) * interval), maximumOffset);
largerOffset = Math.min((int) (Math.ceil(ratio) * interval), maximumOffset);
}
}


// Calculate the nearest offset
// Calculate the nearest offset
int nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
int nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset
? smallerOffset
? smallerOffset
: largerOffset;
: largerOffset;


// Chose the correct snap offset based on velocity
// Chose the correct snap offset based on velocity
if (velocityY > 0) {
if (velocityY > 0) {
targetOffset = largerOffset;
targetOffset = largerOffset;
} else if (velocityY < 0) {
} else if (velocityY < 0) {
targetOffset = smallerOffset;
targetOffset = smallerOffset;
} else {
} else {
targetOffset = nearestOffset;
targetOffset = nearestOffset;
}
}


// Make sure the new offset isn't out of bounds
// Make sure the new offset isn't out of bounds
targetOffset = Math.min(Math.max(0, targetOffset), maximumOffset);
targetOffset = Math.min(Math.max(0, targetOffset), maximumOffset);


// smoothScrollTo will always scroll over 250ms which is often *waaay*
// smoothScrollTo will always scroll over 250ms which is often *waaay*
// too short and will cause the scrolling to feel almost instant
// too short and will cause the scrolling to feel almost instant
// try to manually interact with OverScroller instead
// try to manually interact with OverScroller instead
// if velocity is 0 however, fling() won't work, so we want to use smoothScrollTo
// if velocity is 0 however, fling() won't work, so we want to use smoothScrollTo
if (mScroller != null) {
if (mScroller != null) {
mActivelyScrolling = true;
mActivelyScrolling = true;


mScroller.fling(
mScroller.fling(
getScrollX(), // startX
getScrollX(), // startX
getScrollY(), // startY
getScrollY(), // startY
// velocity = 0 doesn't work with fling() so we pretend there's a reasonable
// 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
// initial velocity going on when a touch is released without any movement
0, // velocityX
0, // velocityX
velocityY != 0 ? velocityY : targetOffset - getScrollY(), // velocityY
velocityY != 0 ? velocityY : targetOffset - getScrollY(), // velocityY
0, // minX
0, // minX
0, // maxX
0, // maxX
// setting both minY and maxY to the same value will guarantee that we scroll to it
// setting both minY and maxY to the same value will guarantee that we scroll to it
// but using the standard fling-style easing rather than smoothScrollTo's 250ms animation
// but using the standard fling-style easing rather than smoothScrollTo's 250ms animation
targetOffset, // minY
targetOffset, // minY
targetOffset, // maxY
targetOffset, // maxY
0, // overX
0, // overX
// we only want to allow overscrolling if the final offset is at the very edge of the view
// we only want to allow overscrolling if the final offset is at the very edge of the view
(targetOffset == 0 || targetOffset == maximumOffset) ? height / 2 : 0 // overY
(targetOffset == 0 || targetOffset == maximumOffset) ? height / 2 : 0 // overY
);
);


postInvalidateOnAnimation();
postInvalidateOnAnimation();
} else {
} else {
smoothScrollTo(getScrollX(), targetOffset);
smoothScrollTo(getScrollX(), targetOffset);
}
}
}
}


private int getSnapInterval() {
private int getSnapInterval() {
if (mSnapInterval != 0) {
if (mSnapInterval != 0) {
return mSnapInterval;
return mSnapInterval;
}
}
return getHeight();
return getHeight();
}
}


public void setEndFillColor(int color) {
public void setEndFillColor(int color) {
if (color != mEndFillColor) {
if (color != mEndFillColor) {
mEndFillColor = color;
mEndFillColor = color;
mEndBackground = new ColorDrawable(mEndFillColor);
mEndBackground = new ColorDrawable(mEndFillColor);
}
}
}
}


@Override
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (mScroller != null) {
if (mScroller != null) {
// FB SCROLLVIEW CHANGE
// FB SCROLLVIEW CHANGE


// This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
// This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
// more information.
// more information.


if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
int scrollRange = getMaxScrollY();
int scrollRange = getMaxScrollY();
if (scrollY >= scrollRange) {
if (scrollY >= scrollRange) {
mScroller.abortAnimation();
mScroller.abortAnimation();
scrollY = scrollRange;
scrollY = scrollRange;
}
}
}
}


// END FB SCROLLVIEW CHANGE
// END FB SCROLLVIEW CHANGE
}
}


super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
}


@Override
@Override
public void onChildViewAdded(View parent, View child) {
public void onChildViewAdded(View parent, View child) {
mContentView = child;
mContentView = child;
mContentView.addOnLayoutChangeListener(this);
mContentView.addOnLayoutChangeListener(this);
}
}


@Override
@Override
public void onChildViewRemoved(View parent, View child) {
public void onChildViewRemoved(View parent, View child) {
mContentView.removeOnLayoutChangeListener(this);
mContentView.removeOnLayoutChangeListener(this);
mContentView = null;
mContentView = null;
}
}


/**
/**
* Called when a mContentView's layout has changed. Fixes the scroll position if it's too large
* Called when a mContentView's layout has changed. Fixes the scroll position if it's too large
* after the content resizes. Without this, the user would see a blank ScrollView when the scroll
* after the content resizes. Without this, the user would see a blank ScrollView when the scroll
* position is larger than the ScrollView's max scroll position after the content shrinks.
* position is larger than the ScrollView's max scroll position after the content shrinks.
*/
*/
@Override
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (mContentView == null) {
if (mContentView == null) {
return;
return;
}
}


int currentScrollY = getScrollY();
int currentScrollY = getScrollY();
int maxScrollY = getMaxScrollY();
int maxScrollY = getMaxScrollY();
if (currentScrollY > maxScrollY) {
if (currentScrollY > maxScrollY) {
scrollTo(getScrollX(), maxScrollY);
scrollTo(getScrollX(), maxScrollY);
}
}
}
}


@Override
@Override
public void setBackgroundColor(int color) {
public void setBackgroundColor(int color) {
mReactBackgroundManager.setBackgroundColor(color);
mReactBackgroundManager.setBackgroundColor(color);
}
}


public void setBorderWidth(int position, float width) {
public void setBorderWidth(int position, float width) {
mReactBackgroundManager.setBorderWidth(position, width);
mReactBackgroundManager.setBorderWidth(position, width);
}
}


public void setBorderColor(int position, float color, float alpha) {
public void setBorderColor(int position, float color, float alpha) {
mReactBackgroundManager.setBorderColor(position, color, alpha);
mReactBackgroundManager.setBorderColor(position, color, alpha);
}
}


public void setBorderRadius(float borderRadius) {
public void setBorderRadius(float borderRadius) {
mReactBackgroundManager.setBorderRadius(borderRadius);
mReactBackgroundManager.setBorderRadius(borderRadius);
}
}


public void setBorderRadius(float borderRadius, int position) {
public void setBorderRadius(float borderRadius, int position) {
mReactBackgroundManager.setBorderRadius(borderRadius, position);
mReactBackgroundManager.setBorderRadius(borderRadius, position);
}
}


public void setBorderStyle(@Nullable String style) {
public void setBorderStyle(@Nullable String style) {
mReactBackgroundManager.setBorderStyle(style);
mReactBackgroundManager.setBorderStyle(style);
}
}


}
}