mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Refactor the ScrollView to move scroll animation into helper and reused in horizontal/vertical scrollview
Summary: This diff refactors the scroll animation from `ReactScrollView` and `ReactHorizontalScrollView` into the `ReactScrollViewHelper` to reduce repeated code. The `Animator` is now shared between all the scroll views in the app, which I believe is the right behavior. It also helps to make the animator changes in future diffs apply to both horizontal and vertical scroll view. - Move `reactSmoothScrollTo` to `smoothScrollTo` in the helper class - This means one Animator for all ScrollViews - Move `updateStateOnScroll` to the helper class - Add interface for accessing instance scroll state properties in ScrollView - This means each ScrollView keeps their own scrolling state - Use `Point` class for pairs of x and y values Changelog: [Internal] Reviewed By: javache Differential Revision: D32372180 fbshipit-source-id: 529180eea788863689c3b440191ed50c5a6f04e5
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5e93cf6885
commit
073195991b
+29
-151
@@ -13,13 +13,10 @@ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNME
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -37,19 +34,18 @@ import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.build.ReactBuildConfig;
|
||||
import com.facebook.react.modules.i18nmanager.I18nUtil;
|
||||
import com.facebook.react.uimanager.FabricViewStateManager;
|
||||
import com.facebook.react.uimanager.MeasureSpecAssertions;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroup;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||
import com.facebook.react.uimanager.ReactOverflowView;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
||||
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasScrollState;
|
||||
import com.facebook.react.views.scroll.ReactScrollViewHelper.ReactScrollViewScrollState;
|
||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
@@ -59,7 +55,8 @@ import java.util.List;
|
||||
public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
implements ReactClippingViewGroup,
|
||||
FabricViewStateManager.HasFabricViewStateManager,
|
||||
ReactOverflowView {
|
||||
ReactOverflowView,
|
||||
HasScrollState {
|
||||
|
||||
private static boolean DEBUG_MODE = false && ReactBuildConfig.DEBUG;
|
||||
private static String TAG = ReactHorizontalScrollView.class.getSimpleName();
|
||||
@@ -68,10 +65,6 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
|
||||
private static @Nullable Field sScrollerField;
|
||||
private static boolean sTriedToGetScrollerField = false;
|
||||
private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft";
|
||||
private static final String CONTENT_OFFSET_TOP = "contentOffsetTop";
|
||||
private static final String SCROLL_AWAY_PADDING_TOP = "scrollAwayPaddingTop";
|
||||
private int mLayoutDirection;
|
||||
|
||||
private int mScrollXAfterMeasure = NO_SCROLL_POSITION;
|
||||
|
||||
@@ -107,13 +100,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
private int pendingContentOffsetX = UNSET_CONTENT_OFFSET;
|
||||
private int pendingContentOffsetY = UNSET_CONTENT_OFFSET;
|
||||
private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager();
|
||||
|
||||
private @Nullable ValueAnimator mScrollAnimator;
|
||||
private int mFinalAnimatedPositionScrollX = 0;
|
||||
private int mFinalAnimatedPositionScrollY = 0;
|
||||
|
||||
private int mLastStateUpdateScrollX = -1;
|
||||
private int mLastStateUpdateScrollY = -1;
|
||||
private final ReactScrollViewScrollState mReactScrollViewScrollState;
|
||||
|
||||
private final Rect mTempRect = new Rect();
|
||||
|
||||
@@ -144,10 +131,11 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
});
|
||||
|
||||
mScroller = getOverScrollerFromParent();
|
||||
mLayoutDirection =
|
||||
I18nUtil.getInstance().isRTL(context)
|
||||
? ViewCompat.LAYOUT_DIRECTION_RTL
|
||||
: ViewCompat.LAYOUT_DIRECTION_LTR;
|
||||
mReactScrollViewScrollState =
|
||||
new ReactScrollViewScrollState(
|
||||
I18nUtil.getInstance().isRTL(context)
|
||||
? ViewCompat.LAYOUT_DIRECTION_RTL
|
||||
: ViewCompat.LAYOUT_DIRECTION_LTR);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -441,7 +429,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
// when JS processes the scroll event, the C++ ShadowNode representation will have a
|
||||
// "more correct" scroll position. It will frequently be /incorrect/ but this decreases
|
||||
// the error as much as possible.
|
||||
updateStateOnScroll();
|
||||
ReactScrollViewHelper.updateStateOnScroll(this);
|
||||
|
||||
ReactScrollViewHelper.emitScrollEvent(
|
||||
this,
|
||||
@@ -525,7 +513,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
mVelocityHelper.calculateVelocity(ev);
|
||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||
if (action == MotionEvent.ACTION_UP && mDragging) {
|
||||
updateStateOnScroll();
|
||||
ReactScrollViewHelper.updateStateOnScroll(this);
|
||||
|
||||
float velocityX = mVelocityHelper.getXVelocity();
|
||||
float velocityY = mVelocityHelper.getYVelocity();
|
||||
@@ -773,7 +761,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
mRunning = true;
|
||||
} else {
|
||||
// There has not been a scroll update since the last time this Runnable executed.
|
||||
updateStateOnScroll();
|
||||
ReactScrollViewHelper.updateStateOnScroll(ReactHorizontalScrollView.this);
|
||||
|
||||
// We keep checking for updates until the ScrollView has "stabilized" and hasn't
|
||||
// scrolled for N consecutive frames. This number is arbitrary: big enough to catch
|
||||
@@ -814,20 +802,6 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
this, mPostTouchRunnable, ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||
}
|
||||
|
||||
/** Get current X position or position after current animation finishes, if any. */
|
||||
private int getPostAnimationScrollX() {
|
||||
return mScrollAnimator != null && mScrollAnimator.isRunning()
|
||||
? mFinalAnimatedPositionScrollX
|
||||
: getScrollX();
|
||||
}
|
||||
|
||||
/** Get current X position or position after current animation finishes, if any. */
|
||||
private int getPostAnimationScrollY() {
|
||||
return mScrollAnimator != null && mScrollAnimator.isRunning()
|
||||
? mFinalAnimatedPositionScrollY
|
||||
: getScrollY();
|
||||
}
|
||||
|
||||
private int predictFinalScrollPosition(int velocityX) {
|
||||
// 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
|
||||
@@ -838,9 +812,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
// predict where a fling would end up so we can scroll to the nearest snap offset
|
||||
int maximumOffset = Math.max(0, computeHorizontalScrollRange() - getWidth());
|
||||
int width = getWidth() - ViewCompat.getPaddingStart(this) - ViewCompat.getPaddingEnd(this);
|
||||
Point postAnimationScroll = ReactScrollViewHelper.getPostAnimationScroll(this);
|
||||
scroller.fling(
|
||||
getPostAnimationScrollX(), // startX
|
||||
getPostAnimationScrollY(), // startY
|
||||
postAnimationScroll.x, // startX
|
||||
postAnimationScroll.y, // startY
|
||||
velocityX, // velocityX
|
||||
0, // velocityY
|
||||
0, // minX
|
||||
@@ -864,7 +839,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
}
|
||||
|
||||
double interval = (double) getSnapInterval();
|
||||
double currentOffset = (double) (getPostAnimationScrollX());
|
||||
double currentOffset = (double) (ReactScrollViewHelper.getPostAnimationScroll(this).x);
|
||||
double targetOffset = (double) predictFinalScrollPosition(velocity);
|
||||
|
||||
int previousPage = (int) Math.floor(currentOffset / interval);
|
||||
@@ -935,9 +910,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
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 (mLayoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
targetOffset = maximumOffset - targetOffset;
|
||||
velocityX = -velocityX;
|
||||
}
|
||||
@@ -1014,7 +990,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
// 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 (mLayoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
currentOffset = maximumOffset - currentOffset;
|
||||
}
|
||||
if (!mSnapToEnd && targetOffset >= lastOffset) {
|
||||
@@ -1048,7 +1024,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
// Make sure the new offset isn't out of bounds
|
||||
targetOffset = Math.min(Math.max(0, targetOffset), maximumOffset);
|
||||
|
||||
if (mLayoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
targetOffset = maximumOffset - targetOffset;
|
||||
velocityX = -velocityX;
|
||||
}
|
||||
@@ -1162,56 +1138,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
* scroll view and state. Calling raw `smoothScrollTo` doesn't update state.
|
||||
*/
|
||||
public void reactSmoothScrollTo(int x, int y) {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(TAG, "reactSmoothScrollTo[%d] x %d y %d", getId(), x, y);
|
||||
}
|
||||
|
||||
// `smoothScrollTo` contains some logic that, if called multiple times in a short amount of
|
||||
// time, will treat all calls as part of the same animation and will not lengthen the duration
|
||||
// of the animation. This means that, for example, if the user is scrolling rapidly, multiple
|
||||
// pages could be considered part of one animation, causing some page animations to be animated
|
||||
// very rapidly - looking like they're not animated at all.
|
||||
if (mScrollAnimator != null) {
|
||||
mScrollAnimator.cancel();
|
||||
}
|
||||
|
||||
mFinalAnimatedPositionScrollX = x;
|
||||
mFinalAnimatedPositionScrollY = y;
|
||||
PropertyValuesHolder scrollX = PropertyValuesHolder.ofInt("scrollX", getScrollX(), x);
|
||||
PropertyValuesHolder scrollY = PropertyValuesHolder.ofInt("scrollY", getScrollY(), y);
|
||||
mScrollAnimator = ObjectAnimator.ofPropertyValuesHolder(scrollX, scrollY);
|
||||
mScrollAnimator.setDuration(
|
||||
ReactScrollViewHelper.getDefaultScrollAnimationDuration(getContext()));
|
||||
mScrollAnimator.addUpdateListener(
|
||||
new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
int scrollValueX = (Integer) valueAnimator.getAnimatedValue("scrollX");
|
||||
int scrollValueY = (Integer) valueAnimator.getAnimatedValue("scrollY");
|
||||
scrollTo(scrollValueX, scrollValueY);
|
||||
}
|
||||
});
|
||||
mScrollAnimator.addListener(
|
||||
new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
mFinalAnimatedPositionScrollX = -1;
|
||||
mFinalAnimatedPositionScrollY = -1;
|
||||
mScrollAnimator = null;
|
||||
updateStateOnScroll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animator) {}
|
||||
});
|
||||
mScrollAnimator.start();
|
||||
updateStateOnScroll(x, y);
|
||||
ReactScrollViewHelper.smoothScrollTo(this, x, y);
|
||||
setPendingContentOffsets(x, y);
|
||||
}
|
||||
|
||||
@@ -1235,7 +1162,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
// to the last item in the list, but that item cannot be move to the start position of the view.
|
||||
final int actualX = getScrollX();
|
||||
final int actualY = getScrollY();
|
||||
updateStateOnScroll(actualX, actualY);
|
||||
ReactScrollViewHelper.updateStateOnScroll(this, actualX, actualY);
|
||||
setPendingContentOffsets(actualX, actualY);
|
||||
}
|
||||
|
||||
@@ -1260,62 +1187,13 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on any stabilized onScroll change to propagate content offset value to a Shadow Node.
|
||||
*/
|
||||
private void updateStateOnScroll(final int scrollX, final int scrollY) {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(TAG, "updateStateOnScroll[%d] scrollX %d scrollY %d", getId(), scrollX, scrollY);
|
||||
}
|
||||
|
||||
// Dedupe events to reduce JNI traffic
|
||||
if (scrollX == mLastStateUpdateScrollX && scrollY == mLastStateUpdateScrollY) {
|
||||
return;
|
||||
}
|
||||
|
||||
mLastStateUpdateScrollX = scrollX;
|
||||
mLastStateUpdateScrollY = scrollY;
|
||||
|
||||
final int fabricScrollX;
|
||||
if (mLayoutDirection == LAYOUT_DIRECTION_RTL) {
|
||||
// getScrollX returns offset from left even when layout direction is RTL.
|
||||
// The following line calculates offset from right.
|
||||
View child = getContentView();
|
||||
int contentWidth = child != null ? child.getWidth() : 0;
|
||||
fabricScrollX = -(contentWidth - scrollX - getWidth());
|
||||
} else {
|
||||
fabricScrollX = scrollX;
|
||||
}
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(
|
||||
TAG,
|
||||
"updateStateOnScroll[%d] scrollX %d scrollY %d fabricScrollX",
|
||||
getId(),
|
||||
scrollX,
|
||||
scrollY,
|
||||
fabricScrollX);
|
||||
}
|
||||
|
||||
mFabricViewStateManager.setState(
|
||||
new FabricViewStateManager.StateUpdateCallback() {
|
||||
@Override
|
||||
public WritableMap getStateUpdate() {
|
||||
WritableMap map = new WritableNativeMap();
|
||||
map.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(fabricScrollX));
|
||||
map.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY));
|
||||
map.putDouble(SCROLL_AWAY_PADDING_TOP, 0);
|
||||
return map;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateStateOnScroll() {
|
||||
updateStateOnScroll(getScrollX(), getScrollY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FabricViewStateManager getFabricViewStateManager() {
|
||||
return mFabricViewStateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactScrollViewScrollState getReactScrollViewScrollState() {
|
||||
return mReactScrollViewScrollState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,10 @@ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNME
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -35,19 +32,16 @@ import androidx.core.view.ViewCompat;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.uimanager.FabricViewStateManager;
|
||||
import com.facebook.react.uimanager.MeasureSpecAssertions;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroup;
|
||||
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
||||
import com.facebook.react.uimanager.ReactOverflowView;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
||||
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasScrollState;
|
||||
import com.facebook.react.views.scroll.ReactScrollViewHelper.ReactScrollViewScrollState;
|
||||
import com.facebook.react.views.view.ReactViewBackgroundManager;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
@@ -64,13 +58,11 @@ public class ReactScrollView extends ScrollView
|
||||
ViewGroup.OnHierarchyChangeListener,
|
||||
View.OnLayoutChangeListener,
|
||||
FabricViewStateManager.HasFabricViewStateManager,
|
||||
ReactOverflowView {
|
||||
ReactOverflowView,
|
||||
HasScrollState {
|
||||
|
||||
private static @Nullable Field sScrollerField;
|
||||
private static boolean sTriedToGetScrollerField = false;
|
||||
private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft";
|
||||
private static final String CONTENT_OFFSET_TOP = "contentOffsetTop";
|
||||
private static final String SCROLL_AWAY_PADDING_TOP = "scrollAwayPaddingTop";
|
||||
|
||||
private static final int UNSET_CONTENT_OFFSET = -1;
|
||||
|
||||
@@ -104,15 +96,8 @@ public class ReactScrollView extends ScrollView
|
||||
private int pendingContentOffsetX = UNSET_CONTENT_OFFSET;
|
||||
private int pendingContentOffsetY = UNSET_CONTENT_OFFSET;
|
||||
private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager();
|
||||
|
||||
private @Nullable ValueAnimator mScrollAnimator;
|
||||
private int mFinalAnimatedPositionScrollX;
|
||||
private int mFinalAnimatedPositionScrollY;
|
||||
|
||||
private int mScrollAwayPaddingTop = 0;
|
||||
|
||||
private int mLastStateUpdateScrollX = -1;
|
||||
private int mLastStateUpdateScrollY = -1;
|
||||
private final ReactScrollViewScrollState mReactScrollViewScrollState =
|
||||
new ReactScrollViewScrollState(ViewCompat.LAYOUT_DIRECTION_LTR);
|
||||
|
||||
public ReactScrollView(Context context) {
|
||||
this(context, null);
|
||||
@@ -324,7 +309,7 @@ public class ReactScrollView extends ScrollView
|
||||
// when JS processes the scroll event, the C++ ShadowNode representation will have a
|
||||
// "more correct" scroll position. It will frequently be /incorrect/ but this decreases
|
||||
// the error as much as possible.
|
||||
updateStateOnScroll();
|
||||
ReactScrollViewHelper.updateStateOnScroll(this);
|
||||
|
||||
ReactScrollViewHelper.emitScrollEvent(
|
||||
this,
|
||||
@@ -366,7 +351,7 @@ public class ReactScrollView extends ScrollView
|
||||
mVelocityHelper.calculateVelocity(ev);
|
||||
int action = ev.getAction() & MotionEvent.ACTION_MASK;
|
||||
if (action == MotionEvent.ACTION_UP && mDragging) {
|
||||
updateStateOnScroll();
|
||||
ReactScrollViewHelper.updateStateOnScroll(this);
|
||||
|
||||
float velocityX = mVelocityHelper.getXVelocity();
|
||||
float velocityY = mVelocityHelper.getYVelocity();
|
||||
@@ -565,7 +550,7 @@ public class ReactScrollView extends ScrollView
|
||||
mRunning = true;
|
||||
} else {
|
||||
// There has not been a scroll update since the last time this Runnable executed.
|
||||
updateStateOnScroll();
|
||||
ReactScrollViewHelper.updateStateOnScroll(ReactScrollView.this);
|
||||
|
||||
// We keep checking for updates until the ScrollView has "stabilized" and hasn't
|
||||
// scrolled for N consecutive frames. This number is arbitrary: big enough to catch
|
||||
@@ -606,20 +591,6 @@ public class ReactScrollView extends ScrollView
|
||||
this, mPostTouchRunnable, ReactScrollViewHelper.MOMENTUM_DELAY);
|
||||
}
|
||||
|
||||
/** Get current X position or position after current animation finishes, if any. */
|
||||
private int getPostAnimationScrollX() {
|
||||
return mScrollAnimator != null && mScrollAnimator.isRunning()
|
||||
? mFinalAnimatedPositionScrollX
|
||||
: getScrollX();
|
||||
}
|
||||
|
||||
/** Get current X position or position after current animation finishes, if any. */
|
||||
private int getPostAnimationScrollY() {
|
||||
return mScrollAnimator != null && mScrollAnimator.isRunning()
|
||||
? mFinalAnimatedPositionScrollY
|
||||
: getScrollY();
|
||||
}
|
||||
|
||||
private int predictFinalScrollPosition(int velocityY) {
|
||||
// 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
|
||||
@@ -630,9 +601,10 @@ public class ReactScrollView extends ScrollView
|
||||
// predict where a fling would end up so we can scroll to the nearest snap offset
|
||||
int maximumOffset = getMaxScrollY();
|
||||
int height = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||
Point postAnimationScroll = ReactScrollViewHelper.getPostAnimationScroll(this);
|
||||
scroller.fling(
|
||||
getPostAnimationScrollX(), // startX
|
||||
getPostAnimationScrollY(), // startY
|
||||
postAnimationScroll.x, // startX
|
||||
postAnimationScroll.y, // startY
|
||||
0, // velocityX
|
||||
velocityY, // velocityY
|
||||
0, // minX
|
||||
@@ -656,7 +628,7 @@ public class ReactScrollView extends ScrollView
|
||||
*/
|
||||
private void smoothScrollAndSnap(int velocity) {
|
||||
double interval = (double) getSnapInterval();
|
||||
double currentOffset = (double) getPostAnimationScrollY();
|
||||
double currentOffset = (double) (ReactScrollViewHelper.getPostAnimationScroll(this).y);
|
||||
double targetOffset = (double) predictFinalScrollPosition(velocity);
|
||||
|
||||
int previousPage = (int) Math.floor(currentOffset / interval);
|
||||
@@ -945,52 +917,7 @@ public class ReactScrollView extends ScrollView
|
||||
* scroll view and state. Calling raw `smoothScrollTo` doesn't update state.
|
||||
*/
|
||||
public void reactSmoothScrollTo(int x, int y) {
|
||||
// `smoothScrollTo` contains some logic that, if called multiple times in a short amount of
|
||||
// time, will treat all calls as part of the same animation and will not lengthen the duration
|
||||
// of the animation. This means that, for example, if the user is scrolling rapidly, multiple
|
||||
// pages could be considered part of one animation, causing some page animations to be animated
|
||||
// very rapidly - looking like they're not animated at all.
|
||||
if (mScrollAnimator != null) {
|
||||
mScrollAnimator.cancel();
|
||||
}
|
||||
|
||||
mFinalAnimatedPositionScrollX = x;
|
||||
mFinalAnimatedPositionScrollY = y;
|
||||
PropertyValuesHolder scrollX = PropertyValuesHolder.ofInt("scrollX", getScrollX(), x);
|
||||
PropertyValuesHolder scrollY = PropertyValuesHolder.ofInt("scrollY", getScrollY(), y);
|
||||
mScrollAnimator = ObjectAnimator.ofPropertyValuesHolder(scrollX, scrollY);
|
||||
mScrollAnimator.setDuration(
|
||||
ReactScrollViewHelper.getDefaultScrollAnimationDuration(getContext()));
|
||||
mScrollAnimator.addUpdateListener(
|
||||
new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
int scrollValueX = (Integer) valueAnimator.getAnimatedValue("scrollX");
|
||||
int scrollValueY = (Integer) valueAnimator.getAnimatedValue("scrollY");
|
||||
scrollTo(scrollValueX, scrollValueY);
|
||||
}
|
||||
});
|
||||
mScrollAnimator.addListener(
|
||||
new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
mFinalAnimatedPositionScrollX = -1;
|
||||
mFinalAnimatedPositionScrollY = -1;
|
||||
mScrollAnimator = null;
|
||||
updateStateOnScroll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animator) {}
|
||||
});
|
||||
mScrollAnimator.start();
|
||||
updateStateOnScroll(x, y);
|
||||
ReactScrollViewHelper.smoothScrollTo(this, x, y);
|
||||
setPendingContentOffsets(x, y);
|
||||
}
|
||||
|
||||
@@ -1010,7 +937,7 @@ public class ReactScrollView extends ScrollView
|
||||
// to the last item in the list, but that item cannot be move to the start position of the view.
|
||||
final int actualX = getScrollX();
|
||||
final int actualY = getScrollY();
|
||||
updateStateOnScroll(actualX, actualY);
|
||||
ReactScrollViewHelper.updateStateOnScroll(this, actualX, actualY);
|
||||
setPendingContentOffsets(actualX, actualY);
|
||||
}
|
||||
|
||||
@@ -1119,61 +1046,18 @@ public class ReactScrollView extends ScrollView
|
||||
setRemoveClippedSubviews(mRemoveClippedSubviews);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on any stabilized onScroll change to propagate content offset value to a Shadow Node.
|
||||
*/
|
||||
private boolean updateStateOnScroll(final int scrollX, final int scrollY) {
|
||||
if (ViewUtil.getUIManagerType(getId()) == UIManagerType.DEFAULT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dedupe events to reduce JNI traffic
|
||||
if (scrollX == mLastStateUpdateScrollX && scrollY == mLastStateUpdateScrollY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mLastStateUpdateScrollX = scrollX;
|
||||
mLastStateUpdateScrollY = scrollY;
|
||||
|
||||
forceUpdateState();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean updateStateOnScroll() {
|
||||
return updateStateOnScroll(getScrollX(), getScrollY());
|
||||
}
|
||||
|
||||
private void updateScrollAwayState(int scrollAwayPaddingTop) {
|
||||
if (mScrollAwayPaddingTop == scrollAwayPaddingTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
mScrollAwayPaddingTop = scrollAwayPaddingTop;
|
||||
|
||||
forceUpdateState();
|
||||
}
|
||||
|
||||
private void forceUpdateState() {
|
||||
final int scrollX = mLastStateUpdateScrollX;
|
||||
final int scrollY = mLastStateUpdateScrollY;
|
||||
final int scrollAwayPaddingTop = mScrollAwayPaddingTop;
|
||||
|
||||
mFabricViewStateManager.setState(
|
||||
new FabricViewStateManager.StateUpdateCallback() {
|
||||
@Override
|
||||
public WritableMap getStateUpdate() {
|
||||
WritableMap map = new WritableNativeMap();
|
||||
map.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(scrollX));
|
||||
map.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY));
|
||||
map.putDouble(SCROLL_AWAY_PADDING_TOP, PixelUtil.toDIPFromPixel(scrollAwayPaddingTop));
|
||||
return map;
|
||||
}
|
||||
});
|
||||
getReactScrollViewScrollState().setScrollAwayPaddingTop(scrollAwayPaddingTop);
|
||||
ReactScrollViewHelper.forceUpdateState(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FabricViewStateManager getFabricViewStateManager() {
|
||||
return mFabricViewStateManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactScrollViewScrollState getReactScrollViewScrollState() {
|
||||
return mReactScrollViewScrollState;
|
||||
}
|
||||
}
|
||||
|
||||
+226
@@ -7,20 +7,38 @@
|
||||
|
||||
package com.facebook.react.views.scroll;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.OverScroller;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.common.build.ReactBuildConfig;
|
||||
import com.facebook.react.uimanager.FabricViewStateManager;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.UIManagerHelper;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/** Helper class that deals with emitting Scroll Events. */
|
||||
public class ReactScrollViewHelper {
|
||||
private static String TAG = ReactHorizontalScrollView.class.getSimpleName();
|
||||
private static boolean DEBUG_MODE = false && ReactBuildConfig.DEBUG;
|
||||
private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft";
|
||||
private static final String CONTENT_OFFSET_TOP = "contentOffsetTop";
|
||||
private static final String SCROLL_AWAY_PADDING_TOP = "scrollAwayPaddingTop";
|
||||
|
||||
public static final long MOMENTUM_DELAY = 20;
|
||||
public static final String OVER_SCROLL_ALWAYS = "always";
|
||||
@@ -32,6 +50,8 @@ public class ReactScrollViewHelper {
|
||||
public static final int SNAP_ALIGNMENT_CENTER = 2;
|
||||
public static final int SNAP_ALIGNMENT_END = 3;
|
||||
|
||||
private static @Nullable ValueAnimator sScrollAnimator;
|
||||
|
||||
public interface ScrollListener {
|
||||
void onScroll(
|
||||
ViewGroup scrollView, ScrollEventType scrollEventType, float xVelocity, float yVelocity);
|
||||
@@ -198,4 +218,210 @@ public class ReactScrollViewHelper {
|
||||
public static void removeScrollListener(ScrollListener listener) {
|
||||
sScrollListeners.remove(listener);
|
||||
}
|
||||
|
||||
public static class ReactScrollViewScrollState {
|
||||
private final int mLayoutDirection;
|
||||
private final Point mFinalAnimatedPositionScroll = new Point();
|
||||
private int mScrollAwayPaddingTop = 0;
|
||||
private final Point mLastStateUpdateScroll = new Point(-1, -1);
|
||||
|
||||
public ReactScrollViewScrollState(final int layoutDirection) {
|
||||
mLayoutDirection = layoutDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the layout direction. Can be either scrollView.LAYOUT_DIRECTION_RTL (1) or
|
||||
* scrollView.LAYOUT_DIRECTION_LTR (0). If the value is -1, it means unknown layout.
|
||||
*/
|
||||
public int getLayoutDirection() {
|
||||
return mLayoutDirection;
|
||||
}
|
||||
|
||||
/** Get the position after current animation is finished */
|
||||
public Point getFinalAnimatedPositionScroll() {
|
||||
return mFinalAnimatedPositionScroll;
|
||||
}
|
||||
|
||||
/** Set the final scroll position after scrolling animation is finished */
|
||||
public ReactScrollViewScrollState setFinalAnimatedPositionScroll(
|
||||
int finalAnimatedPositionScrollX, int finalAnimatedPositionScrollY) {
|
||||
mFinalAnimatedPositionScroll.set(finalAnimatedPositionScrollX, finalAnimatedPositionScrollY);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Get the Fabric state of last scroll position */
|
||||
public Point getLastStateUpdateScroll() {
|
||||
return mLastStateUpdateScroll;
|
||||
}
|
||||
|
||||
/** Set the Fabric state of last scroll position */
|
||||
public ReactScrollViewScrollState setLastStateUpdateScroll(
|
||||
int lastStateUpdateScrollX, int lastStateUpdateScrollY) {
|
||||
mLastStateUpdateScroll.set(lastStateUpdateScrollX, lastStateUpdateScrollY);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Get the padding on the top for nav bar */
|
||||
public int getScrollAwayPaddingTop() {
|
||||
return mScrollAwayPaddingTop;
|
||||
}
|
||||
|
||||
/** Set the padding on the top for nav bar */
|
||||
public ReactScrollViewScrollState setScrollAwayPaddingTop(int scrollAwayPaddingTop) {
|
||||
mScrollAwayPaddingTop = scrollAwayPaddingTop;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static <
|
||||
T extends ViewGroup & FabricViewStateManager.HasFabricViewStateManager & HasScrollState>
|
||||
void smoothScrollTo(final T scrollView, final int x, final int y) {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(TAG, "smoothScrollTo[%d] x %d y %d", scrollView.getId(), x, y);
|
||||
}
|
||||
|
||||
// `smoothScrollTo` contains some logic that, if called multiple times in a short amount of
|
||||
// time, will treat all calls as part of the same animation and will not lengthen the duration
|
||||
// of the animation. This means that, for example, if the user is scrolling rapidly, multiple
|
||||
// pages could be considered part of one animation, causing some page animations to be animated
|
||||
// very rapidly - looking like they're not animated at all.
|
||||
if (sScrollAnimator != null) {
|
||||
sScrollAnimator.cancel();
|
||||
}
|
||||
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
scrollState.setFinalAnimatedPositionScroll(x, y);
|
||||
PropertyValuesHolder scrollX =
|
||||
PropertyValuesHolder.ofInt("scrollX", scrollView.getScrollX(), x);
|
||||
PropertyValuesHolder scrollY =
|
||||
PropertyValuesHolder.ofInt("scrollY", scrollView.getScrollY(), y);
|
||||
sScrollAnimator = ObjectAnimator.ofPropertyValuesHolder(scrollX, scrollY);
|
||||
sScrollAnimator.setDuration(getDefaultScrollAnimationDuration(scrollView.getContext()));
|
||||
sScrollAnimator.addUpdateListener(
|
||||
new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
int scrollValueX = (Integer) valueAnimator.getAnimatedValue("scrollX");
|
||||
int scrollValueY = (Integer) valueAnimator.getAnimatedValue("scrollY");
|
||||
scrollView.scrollTo(scrollValueX, scrollValueY);
|
||||
}
|
||||
});
|
||||
sScrollAnimator.addListener(
|
||||
new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
scrollState.setFinalAnimatedPositionScroll(-1, -1);
|
||||
sScrollAnimator = null;
|
||||
updateStateOnScroll(scrollView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animator) {}
|
||||
});
|
||||
sScrollAnimator.start();
|
||||
updateStateOnScroll(scrollView, x, y);
|
||||
}
|
||||
|
||||
/** Get current (x, y) position or position after current animation finishes, if any. */
|
||||
public static <
|
||||
T extends ViewGroup & FabricViewStateManager.HasFabricViewStateManager & HasScrollState>
|
||||
Point getPostAnimationScroll(final T scrollView) {
|
||||
return sScrollAnimator != null && sScrollAnimator.isRunning()
|
||||
? scrollView.getReactScrollViewScrollState().getFinalAnimatedPositionScroll()
|
||||
: new Point(scrollView.getScrollX(), scrollView.getScrollY());
|
||||
}
|
||||
|
||||
public static <
|
||||
T extends ViewGroup & FabricViewStateManager.HasFabricViewStateManager & HasScrollState>
|
||||
boolean updateStateOnScroll(final T scrollView) {
|
||||
return updateStateOnScroll(scrollView, scrollView.getScrollX(), scrollView.getScrollY());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on any stabilized onScroll change to propagate content offset value to a Shadow Node.
|
||||
*/
|
||||
public static <
|
||||
T extends ViewGroup & FabricViewStateManager.HasFabricViewStateManager & HasScrollState>
|
||||
boolean updateStateOnScroll(final T scrollView, final int scrollX, final int scrollY) {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(
|
||||
TAG,
|
||||
"updateStateOnScroll[%d] scrollX %d scrollY %d",
|
||||
scrollView.getId(),
|
||||
scrollX,
|
||||
scrollY);
|
||||
}
|
||||
|
||||
if (ViewUtil.getUIManagerType(scrollView.getId()) == UIManagerType.DEFAULT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
// Dedupe events to reduce JNI traffic
|
||||
if (scrollState.getLastStateUpdateScroll().equals(scrollX, scrollY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
scrollState.setLastStateUpdateScroll(scrollX, scrollY);
|
||||
forceUpdateState(scrollView);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static <
|
||||
T extends ViewGroup & FabricViewStateManager.HasFabricViewStateManager & HasScrollState>
|
||||
void forceUpdateState(final T scrollView) {
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
final int scrollAwayPaddingTop = scrollState.getScrollAwayPaddingTop();
|
||||
final Point scrollPos = scrollState.getLastStateUpdateScroll();
|
||||
final int scrollX = scrollPos.x;
|
||||
final int scrollY = scrollPos.y;
|
||||
final int fabricScrollX;
|
||||
int layoutDirection = scrollState.getLayoutDirection();
|
||||
|
||||
if (layoutDirection == scrollView.LAYOUT_DIRECTION_RTL) {
|
||||
// getScrollX returns offset from left even when layout direction is RTL.
|
||||
// The following line calculates offset from right.
|
||||
View child = scrollView.getChildAt(0);
|
||||
int contentWidth = child != null ? child.getWidth() : 0;
|
||||
fabricScrollX = -(contentWidth - scrollX - scrollView.getWidth());
|
||||
} else {
|
||||
fabricScrollX = scrollX;
|
||||
}
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(
|
||||
TAG,
|
||||
"updateStateOnScroll[%d] scrollX %d scrollY %d fabricScrollX",
|
||||
scrollView.getId(),
|
||||
scrollX,
|
||||
scrollY,
|
||||
fabricScrollX);
|
||||
}
|
||||
|
||||
scrollView
|
||||
.getFabricViewStateManager()
|
||||
.setState(
|
||||
new FabricViewStateManager.StateUpdateCallback() {
|
||||
@Override
|
||||
public WritableMap getStateUpdate() {
|
||||
WritableMap map = new WritableNativeMap();
|
||||
map.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(scrollX));
|
||||
map.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY));
|
||||
map.putDouble(
|
||||
SCROLL_AWAY_PADDING_TOP, PixelUtil.toDIPFromPixel(scrollAwayPaddingTop));
|
||||
return map;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface HasScrollState {
|
||||
/** Get the scroll state for the current ScrollView */
|
||||
ReactScrollViewScrollState getReactScrollViewScrollState();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user