Skip to content

Commit

Permalink
Refactor the ScrollView to move scroll animation into helper and reus…
Browse files Browse the repository at this point in the history
…ed 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
  • Loading branch information
ryancat authored and facebook-github-bot committed Nov 18, 2021
1 parent 5e93cf6 commit 0731959
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 290 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@
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;
Expand All @@ -37,19 +34,18 @@
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;
Expand All @@ -59,7 +55,8 @@
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();
Expand All @@ -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;

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -144,10 +131,11 @@ public void onInitializeAccessibilityNodeInfo(
});

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
Expand Down Expand Up @@ -441,7 +429,7 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) {
// 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,
Expand Down Expand Up @@ -525,7 +513,7 @@ public boolean onTouchEvent(MotionEvent ev) {
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();
Expand Down Expand Up @@ -773,7 +761,7 @@ public void run() {
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
Expand Down Expand Up @@ -814,20 +802,6 @@ public void run() {
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
Expand All @@ -838,9 +812,10 @@ private int predictFinalScrollPosition(int velocityX) {
// 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
Expand All @@ -864,7 +839,7 @@ private void smoothScrollAndSnap(int velocity) {
}

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);
Expand Down Expand Up @@ -935,9 +910,10 @@ private void flingAndSnap(int velocityX) {
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;
}
Expand Down Expand Up @@ -1014,7 +990,7 @@ private void flingAndSnap(int velocityX) {
// 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) {
Expand Down Expand Up @@ -1048,7 +1024,7 @@ private void flingAndSnap(int velocityX) {
// 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;
}
Expand Down Expand Up @@ -1162,56 +1138,7 @@ public void setBorderStyle(@Nullable String style) {
* 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);
}

Expand All @@ -1235,7 +1162,7 @@ public void scrollTo(int x, int y) {
// 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);
}

Expand All @@ -1260,62 +1187,13 @@ private void setPendingContentOffsets(int x, int y) {
}
}

/**
* 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;
}
}
Loading

0 comments on commit 0731959

Please sign in to comment.