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:
Xin Chen
2021-11-18 14:38:23 -08:00
committed by Facebook GitHub Bot
parent 5e93cf6885
commit 073195991b
3 changed files with 278 additions and 290 deletions
@@ -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;
}
}
@@ -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();
}
}