mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
ReactScrollViewHelper -> Kotlin (#43735)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43735 ## Changelog: [Internal] - As in the title. Reviewed By: tdn120 Differential Revision: D55576494 fbshipit-source-id: ee056b6ba629614a65440f68ad4dee5f53674879
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8ad308e847
commit
c35cc43a6c
@@ -6648,8 +6648,9 @@ public class com/facebook/react/views/scroll/ReactScrollViewCommandHelper$Scroll
|
||||
public final field mAnimated Z
|
||||
}
|
||||
|
||||
public class com/facebook/react/views/scroll/ReactScrollViewHelper {
|
||||
public final class com/facebook/react/views/scroll/ReactScrollViewHelper {
|
||||
public static final field AUTO Ljava/lang/String;
|
||||
public static final field INSTANCE Lcom/facebook/react/views/scroll/ReactScrollViewHelper;
|
||||
public static final field MOMENTUM_DELAY J
|
||||
public static final field OVER_SCROLL_ALWAYS Ljava/lang/String;
|
||||
public static final field OVER_SCROLL_NEVER Ljava/lang/String;
|
||||
@@ -6657,26 +6658,25 @@ public class com/facebook/react/views/scroll/ReactScrollViewHelper {
|
||||
public static final field SNAP_ALIGNMENT_DISABLED I
|
||||
public static final field SNAP_ALIGNMENT_END I
|
||||
public static final field SNAP_ALIGNMENT_START I
|
||||
public fun <init> ()V
|
||||
public static fun addScrollListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener;)V
|
||||
public static fun emitLayoutEvent (Landroid/view/ViewGroup;)V
|
||||
public static fun emitScrollBeginDragEvent (Landroid/view/ViewGroup;)V
|
||||
public static fun emitScrollEndDragEvent (Landroid/view/ViewGroup;FF)V
|
||||
public static fun emitScrollEvent (Landroid/view/ViewGroup;FF)V
|
||||
public static fun emitScrollMomentumBeginEvent (Landroid/view/ViewGroup;II)V
|
||||
public static fun emitScrollMomentumEndEvent (Landroid/view/ViewGroup;)V
|
||||
public static fun forceUpdateState (Landroid/view/ViewGroup;)V
|
||||
public static fun getDefaultScrollAnimationDuration (Landroid/content/Context;)I
|
||||
public static fun getNextFlingStartValue (Landroid/view/ViewGroup;III)I
|
||||
public static fun parseOverScrollMode (Ljava/lang/String;)I
|
||||
public static fun parseSnapToAlignment (Ljava/lang/String;)I
|
||||
public static fun predictFinalScrollPosition (Landroid/view/ViewGroup;IIII)Landroid/graphics/Point;
|
||||
public static fun registerFlingAnimator (Landroid/view/ViewGroup;)V
|
||||
public static fun removeScrollListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener;)V
|
||||
public static fun smoothScrollTo (Landroid/view/ViewGroup;II)V
|
||||
public static fun updateFabricScrollState (Landroid/view/ViewGroup;)V
|
||||
public static fun updateFabricScrollState (Landroid/view/ViewGroup;II)V
|
||||
public static fun updateStateOnScrollChanged (Landroid/view/ViewGroup;FF)V
|
||||
public static final fun addScrollListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener;)V
|
||||
public static final fun emitLayoutEvent (Landroid/view/ViewGroup;)V
|
||||
public static final fun emitScrollBeginDragEvent (Landroid/view/ViewGroup;)V
|
||||
public static final fun emitScrollEndDragEvent (Landroid/view/ViewGroup;FF)V
|
||||
public static final fun emitScrollEvent (Landroid/view/ViewGroup;FF)V
|
||||
public static final fun emitScrollMomentumBeginEvent (Landroid/view/ViewGroup;II)V
|
||||
public static final fun emitScrollMomentumEndEvent (Landroid/view/ViewGroup;)V
|
||||
public static final fun forceUpdateState (Landroid/view/ViewGroup;)V
|
||||
public static final fun getDefaultScrollAnimationDuration (Landroid/content/Context;)I
|
||||
public static final fun getNextFlingStartValue (Landroid/view/ViewGroup;III)I
|
||||
public static final fun parseOverScrollMode (Ljava/lang/String;)I
|
||||
public static final fun parseSnapToAlignment (Ljava/lang/String;)I
|
||||
public static final fun predictFinalScrollPosition (Landroid/view/ViewGroup;IIII)Landroid/graphics/Point;
|
||||
public final fun registerFlingAnimator (Landroid/view/ViewGroup;)V
|
||||
public static final fun removeScrollListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener;)V
|
||||
public static final fun smoothScrollTo (Landroid/view/ViewGroup;II)V
|
||||
public static final fun updateFabricScrollState (Landroid/view/ViewGroup;)V
|
||||
public final fun updateFabricScrollState (Landroid/view/ViewGroup;II)V
|
||||
public static final fun updateStateOnScrollChanged (Landroid/view/ViewGroup;FF)V
|
||||
}
|
||||
|
||||
public abstract interface class com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator {
|
||||
@@ -6705,21 +6705,21 @@ public abstract interface class com/facebook/react/views/scroll/ReactScrollViewH
|
||||
public abstract fun getStateWrapper ()Lcom/facebook/react/uimanager/StateWrapper;
|
||||
}
|
||||
|
||||
public class com/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState {
|
||||
public final class com/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState {
|
||||
public fun <init> (I)V
|
||||
public fun getDecelerationRate ()F
|
||||
public fun getFinalAnimatedPositionScroll ()Landroid/graphics/Point;
|
||||
public fun getIsCanceled ()Z
|
||||
public fun getIsFinished ()Z
|
||||
public fun getLastStateUpdateScroll ()Landroid/graphics/Point;
|
||||
public fun getLayoutDirection ()I
|
||||
public fun getScrollAwayPaddingTop ()I
|
||||
public fun setDecelerationRate (F)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public fun setFinalAnimatedPositionScroll (II)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public fun setIsCanceled (Z)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public fun setIsFinished (Z)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public fun setLastStateUpdateScroll (II)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public fun setScrollAwayPaddingTop (I)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public final fun getDecelerationRate ()F
|
||||
public final fun getFinalAnimatedPositionScroll ()Landroid/graphics/Point;
|
||||
public final fun getLastStateUpdateScroll ()Landroid/graphics/Point;
|
||||
public final fun getLayoutDirection ()I
|
||||
public final fun getScrollAwayPaddingTop ()I
|
||||
public final fun isCanceled ()Z
|
||||
public final fun isFinished ()Z
|
||||
public final fun setCanceled (Z)V
|
||||
public final fun setDecelerationRate (F)V
|
||||
public final fun setFinalAnimatedPositionScroll (II)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public final fun setFinished (Z)V
|
||||
public final fun setLastStateUpdateScroll (II)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;
|
||||
public final fun setScrollAwayPaddingTop (I)V
|
||||
}
|
||||
|
||||
public abstract interface class com/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener {
|
||||
|
||||
-603
@@ -1,603 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.scroll;
|
||||
|
||||
import android.animation.Animator;
|
||||
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 androidx.core.view.ViewCompat;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
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.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.StateWrapper;
|
||||
import com.facebook.react.uimanager.UIManagerHelper;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/** Helper class that deals with emitting Scroll Events. */
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
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";
|
||||
public static final String AUTO = "auto";
|
||||
public static final String OVER_SCROLL_NEVER = "never";
|
||||
|
||||
public static final int SNAP_ALIGNMENT_DISABLED = 0;
|
||||
public static final int SNAP_ALIGNMENT_START = 1;
|
||||
public static final int SNAP_ALIGNMENT_CENTER = 2;
|
||||
public static final int SNAP_ALIGNMENT_END = 3;
|
||||
|
||||
public interface ScrollListener {
|
||||
void onScroll(
|
||||
ViewGroup scrollView, ScrollEventType scrollEventType, float xVelocity, float yVelocity);
|
||||
|
||||
void onLayout(ViewGroup scrollView);
|
||||
}
|
||||
|
||||
public interface HasStateWrapper {
|
||||
@Nullable
|
||||
StateWrapper getStateWrapper();
|
||||
}
|
||||
|
||||
// Support global native listeners for scroll events
|
||||
private static final Set<ScrollListener> sScrollListeners =
|
||||
Collections.newSetFromMap(new WeakHashMap<ScrollListener, Boolean>());
|
||||
|
||||
// If all else fails, this is the hardcoded value in OverScroller.java, in AOSP.
|
||||
// The default is defined here (as of this diff):
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/ae5bcf23b5f0875e455790d6af387184dbd009c1/core/java/android/widget/OverScroller.java#44
|
||||
private static int SMOOTH_SCROLL_DURATION = 250;
|
||||
private static boolean mSmoothScrollDurationInitialized = false;
|
||||
|
||||
/** Shared by {@link ReactScrollView} and {@link ReactHorizontalScrollView}. */
|
||||
public static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollEvent(
|
||||
T scrollView, float xVelocity, float yVelocity) {
|
||||
emitScrollEvent(scrollView, ScrollEventType.SCROLL, xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollBeginDragEvent(
|
||||
T scrollView) {
|
||||
emitScrollEvent(scrollView, ScrollEventType.BEGIN_DRAG);
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollEndDragEvent(
|
||||
T scrollView, float xVelocity, float yVelocity) {
|
||||
emitScrollEvent(scrollView, ScrollEventType.END_DRAG, xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollMomentumBeginEvent(
|
||||
T scrollView, int xVelocity, int yVelocity) {
|
||||
emitScrollEvent(scrollView, ScrollEventType.MOMENTUM_BEGIN, xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollMomentumEndEvent(
|
||||
T scrollView) {
|
||||
emitScrollEvent(scrollView, ScrollEventType.MOMENTUM_END);
|
||||
}
|
||||
|
||||
private static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollEvent(
|
||||
T scrollView, ScrollEventType scrollEventType) {
|
||||
emitScrollEvent(scrollView, scrollEventType, 0, 0);
|
||||
}
|
||||
|
||||
private static <T extends ViewGroup & HasScrollEventThrottle> void emitScrollEvent(
|
||||
T scrollView, ScrollEventType scrollEventType, float xVelocity, float yVelocity) {
|
||||
long now = System.currentTimeMillis();
|
||||
// Throttle the scroll event if scrollEventThrottle is set to be equal or more than 17 ms.
|
||||
// We limit the delta to 17ms so that small throttles intended to enable 60fps updates will not
|
||||
// inadvertently filter out any scroll events.
|
||||
if (scrollView.getScrollEventThrottle()
|
||||
>= Math.max(17, now - scrollView.getLastScrollDispatchTime())) {
|
||||
// Scroll events are throttled.
|
||||
return;
|
||||
}
|
||||
|
||||
View contentView = scrollView.getChildAt(0);
|
||||
|
||||
if (contentView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ScrollListener scrollListener : sScrollListeners) {
|
||||
scrollListener.onScroll(scrollView, scrollEventType, xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
ReactContext reactContext = (ReactContext) scrollView.getContext();
|
||||
int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
|
||||
|
||||
// It's possible for the EventDispatcher to go away - for example,
|
||||
// if there's a crash initiated from JS and we tap on a ScrollView
|
||||
// around teardown of RN, this will cause a NPE. We can safely ignore
|
||||
// this since the crash is usually a red herring.
|
||||
EventDispatcher eventDispatcher =
|
||||
UIManagerHelper.getEventDispatcherForReactTag(reactContext, scrollView.getId());
|
||||
if (eventDispatcher != null) {
|
||||
eventDispatcher.dispatchEvent(
|
||||
ScrollEvent.obtain(
|
||||
surfaceId,
|
||||
scrollView.getId(),
|
||||
scrollEventType,
|
||||
scrollView.getScrollX(),
|
||||
scrollView.getScrollY(),
|
||||
xVelocity,
|
||||
yVelocity,
|
||||
contentView.getWidth(),
|
||||
contentView.getHeight(),
|
||||
scrollView.getWidth(),
|
||||
scrollView.getHeight()));
|
||||
scrollView.setLastScrollDispatchTime(now);
|
||||
}
|
||||
}
|
||||
|
||||
/** This is only for Java listeners. onLayout events emitted to JS are handled elsewhere. */
|
||||
public static void emitLayoutEvent(ViewGroup scrollView) {
|
||||
for (ScrollListener scrollListener : sScrollListeners) {
|
||||
scrollListener.onLayout(scrollView);
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseOverScrollMode(String jsOverScrollMode) {
|
||||
if (jsOverScrollMode == null || jsOverScrollMode.equals(AUTO)) {
|
||||
return View.OVER_SCROLL_IF_CONTENT_SCROLLS;
|
||||
} else if (jsOverScrollMode.equals(OVER_SCROLL_ALWAYS)) {
|
||||
return View.OVER_SCROLL_ALWAYS;
|
||||
} else if (jsOverScrollMode.equals(OVER_SCROLL_NEVER)) {
|
||||
return View.OVER_SCROLL_NEVER;
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "wrong overScrollMode: " + jsOverScrollMode);
|
||||
return View.OVER_SCROLL_IF_CONTENT_SCROLLS;
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseSnapToAlignment(@Nullable String alignment) {
|
||||
if (alignment == null) {
|
||||
return SNAP_ALIGNMENT_DISABLED;
|
||||
} else if ("start".equalsIgnoreCase(alignment)) {
|
||||
return SNAP_ALIGNMENT_START;
|
||||
} else if ("center".equalsIgnoreCase(alignment)) {
|
||||
return SNAP_ALIGNMENT_CENTER;
|
||||
} else if ("end".equals(alignment)) {
|
||||
return SNAP_ALIGNMENT_END;
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "wrong snap alignment value: " + alignment);
|
||||
return SNAP_ALIGNMENT_DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDefaultScrollAnimationDuration(Context context) {
|
||||
if (!mSmoothScrollDurationInitialized) {
|
||||
mSmoothScrollDurationInitialized = true;
|
||||
|
||||
try {
|
||||
OverScrollerDurationGetter overScrollerDurationGetter =
|
||||
new OverScrollerDurationGetter(context);
|
||||
SMOOTH_SCROLL_DURATION = overScrollerDurationGetter.getScrollAnimationDuration();
|
||||
} catch (Throwable e) {
|
||||
}
|
||||
}
|
||||
|
||||
return SMOOTH_SCROLL_DURATION;
|
||||
}
|
||||
|
||||
private static class OverScrollerDurationGetter extends OverScroller {
|
||||
// This is the default in AOSP, hardcoded in OverScroller.java.
|
||||
private int mScrollAnimationDuration = 250;
|
||||
|
||||
OverScrollerDurationGetter(Context context) {
|
||||
// We call with a null context because OverScroller does not use the context
|
||||
// in the execution path we're interested in, unless heavily modified in an AOSP fork.
|
||||
super(context);
|
||||
}
|
||||
|
||||
public int getScrollAnimationDuration() {
|
||||
// If startScroll is called without a duration, OverScroller will call `startScroll(x, y, dx,
|
||||
// dy, duration)` with the default duration.
|
||||
super.startScroll(0, 0, 0, 0);
|
||||
|
||||
return mScrollAnimationDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
|
||||
mScrollAnimationDuration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scroll listener.
|
||||
*
|
||||
* <p>Note that you must keep a reference to this scroll listener because this class only keeps a
|
||||
* weak reference to it (to prevent memory leaks). This means that code like <code>
|
||||
* addScrollListener(new ScrollListener() {...})</code> won't work, you need to do this instead:
|
||||
* <code>
|
||||
* mScrollListener = new ScrollListener() {...};
|
||||
* ReactScrollViewHelper.addScrollListener(mScrollListener);
|
||||
* </code> instead.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public static void addScrollListener(ScrollListener listener) {
|
||||
sScrollListeners.add(listener);
|
||||
}
|
||||
|
||||
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);
|
||||
private boolean mIsCanceled = false;
|
||||
private boolean mIsFinished = true;
|
||||
private float mDecelerationRate = 0.985f;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** Get true if the previous animation was canceled */
|
||||
public boolean getIsCanceled() {
|
||||
return mIsCanceled;
|
||||
}
|
||||
|
||||
/** Set the state of current animation is canceled or not */
|
||||
public ReactScrollViewScrollState setIsCanceled(boolean isCanceled) {
|
||||
mIsCanceled = isCanceled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Get true if previous animation was finished */
|
||||
public boolean getIsFinished() {
|
||||
return mIsFinished;
|
||||
}
|
||||
|
||||
/** Set the state of current animation is finished or not */
|
||||
public ReactScrollViewScrollState setIsFinished(boolean isFinished) {
|
||||
mIsFinished = isFinished;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Get true if previous animation was finished */
|
||||
public float getDecelerationRate() {
|
||||
return mDecelerationRate;
|
||||
}
|
||||
|
||||
/** Set the state of current animation is finished or not */
|
||||
public ReactScrollViewScrollState setDecelerationRate(float decelerationRate) {
|
||||
mDecelerationRate = decelerationRate;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the given view to the location (x, y), with provided initial velocity. This method works
|
||||
* by calculate the "would be" initial velocity with internal friction to move to the point (x,
|
||||
* y), then apply that to the animator.
|
||||
*/
|
||||
public static <T extends ViewGroup & HasStateWrapper & HasScrollState & HasFlingAnimator>
|
||||
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);
|
||||
}
|
||||
|
||||
// Register the listeners for the fling animator if there isn't any
|
||||
final ValueAnimator flingAnimator = scrollView.getFlingAnimator();
|
||||
if (flingAnimator.getListeners() == null || flingAnimator.getListeners().size() == 0) {
|
||||
registerFlingAnimator(scrollView);
|
||||
}
|
||||
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
scrollState.setFinalAnimatedPositionScroll(x, y);
|
||||
|
||||
final int scrollX = scrollView.getScrollX();
|
||||
final int scrollY = scrollView.getScrollY();
|
||||
// Only one fling animator will be started. For the horizontal scroll view, scrollY will always
|
||||
// be the same to y. This is the same to the vertical scroll view.
|
||||
if (scrollX != x) {
|
||||
scrollView.startFlingAnimator(scrollX, x);
|
||||
}
|
||||
if (scrollY != y) {
|
||||
scrollView.startFlingAnimator(scrollY, y);
|
||||
}
|
||||
|
||||
updateFabricScrollState(scrollView, x, y);
|
||||
}
|
||||
|
||||
/** Get current position or position after current animation finishes, if any. */
|
||||
public static <T extends ViewGroup & HasScrollState & HasFlingAnimator>
|
||||
int getNextFlingStartValue(
|
||||
final T scrollView,
|
||||
final int currentValue,
|
||||
final int postAnimationValue,
|
||||
final int velocity) {
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
final int velocityDirectionMask = velocity != 0 ? velocity / Math.abs(velocity) : 0;
|
||||
final boolean isMovingTowardsAnimatedValue =
|
||||
velocityDirectionMask * (postAnimationValue - currentValue) > 0;
|
||||
|
||||
// When the fling animation is not finished, or it was canceled and now we are moving towards
|
||||
// the final animated value, we will return the final animated value. This is because follow up
|
||||
// animation should consider the "would be" animated location, so that previous quick small
|
||||
// scrolls are still working.
|
||||
return !scrollState.getIsFinished()
|
||||
|| (scrollState.getIsCanceled() && isMovingTowardsAnimatedValue)
|
||||
? postAnimationValue
|
||||
: currentValue;
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasStateWrapper & HasScrollState & HasFlingAnimator>
|
||||
void updateFabricScrollState(final T scrollView) {
|
||||
updateFabricScrollState(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 & HasStateWrapper & HasScrollState & HasFlingAnimator>
|
||||
void updateFabricScrollState(final T scrollView, final int scrollX, final int scrollY) {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(
|
||||
TAG,
|
||||
"updateFabricScrollState[%d] scrollX %d scrollY %d",
|
||||
scrollView.getId(),
|
||||
scrollX,
|
||||
scrollY);
|
||||
}
|
||||
|
||||
if (ViewUtil.getUIManagerType(scrollView.getId()) == UIManagerType.DEFAULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
// Dedupe events to reduce JNI traffic
|
||||
if (scrollState.getLastStateUpdateScroll().equals(scrollX, scrollY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollState.setLastStateUpdateScroll(scrollX, scrollY);
|
||||
forceUpdateState(scrollView);
|
||||
return;
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasScrollState & HasStateWrapper & HasFlingAnimator>
|
||||
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,
|
||||
"updateFabricScrollState[%d] scrollX %d scrollY %d fabricScrollX",
|
||||
scrollView.getId(),
|
||||
scrollX,
|
||||
scrollY,
|
||||
fabricScrollX);
|
||||
}
|
||||
|
||||
StateWrapper stateWrapper = scrollView.getStateWrapper();
|
||||
if (stateWrapper != null) {
|
||||
WritableMap newStateData = new WritableNativeMap();
|
||||
newStateData.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(scrollX));
|
||||
newStateData.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY));
|
||||
newStateData.putDouble(
|
||||
SCROLL_AWAY_PADDING_TOP, PixelUtil.toDIPFromPixel(scrollAwayPaddingTop));
|
||||
stateWrapper.updateState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
public static <
|
||||
T extends
|
||||
ViewGroup & HasStateWrapper & HasScrollState & HasFlingAnimator
|
||||
& HasScrollEventThrottle>
|
||||
void updateStateOnScrollChanged(
|
||||
final T scrollView, final float xVelocity, final float yVelocity) {
|
||||
// Race an UpdateState with every onScroll. This makes it more likely that, in Fabric,
|
||||
// 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.
|
||||
updateFabricScrollState(scrollView);
|
||||
emitScrollEvent(scrollView, xVelocity, yVelocity);
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasStateWrapper & HasScrollState & HasFlingAnimator>
|
||||
void registerFlingAnimator(final T scrollView) {
|
||||
scrollView
|
||||
.getFlingAnimator()
|
||||
.addListener(
|
||||
new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
final ReactScrollViewScrollState scrollState =
|
||||
scrollView.getReactScrollViewScrollState();
|
||||
scrollState.setIsCanceled(false);
|
||||
scrollState.setIsFinished(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
scrollView.getReactScrollViewScrollState().setIsFinished(true);
|
||||
updateFabricScrollState(scrollView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {
|
||||
scrollView.getReactScrollViewScrollState().setIsCanceled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animator) {}
|
||||
});
|
||||
}
|
||||
|
||||
public static <T extends ViewGroup & HasScrollState & HasFlingAnimator>
|
||||
Point predictFinalScrollPosition(
|
||||
final T scrollView,
|
||||
final int velocityX,
|
||||
final int velocityY,
|
||||
final int maximumOffsetX,
|
||||
final int maximumOffsetY) {
|
||||
final ReactScrollViewScrollState scrollState = scrollView.getReactScrollViewScrollState();
|
||||
// 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
|
||||
// so we can predict where a fling would land and snap to nearby that point.
|
||||
OverScroller scroller = new OverScroller(scrollView.getContext());
|
||||
scroller.setFriction(1.0f - scrollState.getDecelerationRate());
|
||||
|
||||
// predict where a fling would end up so we can scroll to the nearest snap offset
|
||||
int width =
|
||||
scrollView.getWidth()
|
||||
- ViewCompat.getPaddingStart(scrollView)
|
||||
- ViewCompat.getPaddingEnd(scrollView);
|
||||
int height =
|
||||
scrollView.getHeight() - scrollView.getPaddingBottom() - scrollView.getPaddingTop();
|
||||
Point finalAnimatedPositionScroll = scrollState.getFinalAnimatedPositionScroll();
|
||||
scroller.fling(
|
||||
getNextFlingStartValue(
|
||||
scrollView,
|
||||
scrollView.getScrollX(),
|
||||
finalAnimatedPositionScroll.x,
|
||||
velocityX), // startX
|
||||
getNextFlingStartValue(
|
||||
scrollView,
|
||||
scrollView.getScrollY(),
|
||||
finalAnimatedPositionScroll.y,
|
||||
velocityY), // startY
|
||||
velocityX, // velocityX
|
||||
velocityY, // velocityY
|
||||
0, // minX
|
||||
maximumOffsetX, // maxX
|
||||
0, // minY
|
||||
maximumOffsetY, // maxY
|
||||
width / 2, // overX
|
||||
height / 2 // overY
|
||||
);
|
||||
return new Point(scroller.getFinalX(), scroller.getFinalY());
|
||||
}
|
||||
|
||||
public interface HasScrollState {
|
||||
/** Get the scroll state for the current ScrollView */
|
||||
ReactScrollViewScrollState getReactScrollViewScrollState();
|
||||
}
|
||||
|
||||
public interface HasFlingAnimator {
|
||||
/**
|
||||
* Start the fling animator that the ScrollView has to go from the start position to end
|
||||
* position.
|
||||
*/
|
||||
void startFlingAnimator(int start, int end);
|
||||
|
||||
/** Get the fling animator that is reused for the ScrollView to handle fling animation. */
|
||||
ValueAnimator getFlingAnimator();
|
||||
|
||||
/** Get the fling distance with current velocity for prediction */
|
||||
int getFlingExtrapolatedDistance(int velocity);
|
||||
}
|
||||
|
||||
public interface HasScrollEventThrottle {
|
||||
/**
|
||||
* Set the scroll event throttle in ms. This number is used to throttle the scroll events. The
|
||||
* default value is zero, which means the scroll events are sent with no throttle.
|
||||
*/
|
||||
void setScrollEventThrottle(int scrollEventThrottle);
|
||||
|
||||
/** Get the scroll event throttle in ms. */
|
||||
int getScrollEventThrottle();
|
||||
|
||||
/** Set the scroll view's last dispatch time for throttling */
|
||||
void setLastScrollDispatchTime(long lastScrollDispatchTime);
|
||||
|
||||
/** Get the scroll view dispatch time for throttling */
|
||||
long getLastScrollDispatchTime();
|
||||
}
|
||||
|
||||
public interface HasSmoothScroll {
|
||||
void reactSmoothScrollTo(int x, int y);
|
||||
|
||||
void scrollToPreservingMomentum(int x, int y);
|
||||
}
|
||||
}
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.scroll
|
||||
|
||||
import android.animation.Animator
|
||||
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.core.view.ViewCompat
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.WritableNativeMap
|
||||
import com.facebook.react.common.ReactConstants
|
||||
import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel
|
||||
import com.facebook.react.uimanager.StateWrapper
|
||||
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.WeakHashMap
|
||||
|
||||
/** Helper class that deals with emitting Scroll Events. */
|
||||
public object ReactScrollViewHelper {
|
||||
private val TAG = ReactHorizontalScrollView::class.java.simpleName
|
||||
private val DEBUG_MODE = false // ReactBuildConfig.DEBUG
|
||||
private const val CONTENT_OFFSET_LEFT = "contentOffsetLeft"
|
||||
private const val CONTENT_OFFSET_TOP = "contentOffsetTop"
|
||||
private const val SCROLL_AWAY_PADDING_TOP = "scrollAwayPaddingTop"
|
||||
|
||||
public const val MOMENTUM_DELAY: Long = 20
|
||||
public const val OVER_SCROLL_ALWAYS: String = "always"
|
||||
public const val AUTO: String = "auto"
|
||||
public const val OVER_SCROLL_NEVER: String = "never"
|
||||
public const val SNAP_ALIGNMENT_DISABLED: Int = 0
|
||||
public const val SNAP_ALIGNMENT_START: Int = 1
|
||||
public const val SNAP_ALIGNMENT_CENTER: Int = 2
|
||||
public const val SNAP_ALIGNMENT_END: Int = 3
|
||||
|
||||
// Support global native listeners for scroll events
|
||||
private val scrollListeners = Collections.newSetFromMap(WeakHashMap<ScrollListener, Boolean>())
|
||||
|
||||
// If all else fails, this is the hardcoded value in OverScroller.java, in AOSP.
|
||||
// The default is defined here (as of this diff):
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/ae5bcf23b5f0875e455790d6af387184dbd009c1/core/java/android/widget/OverScroller.java#44
|
||||
private var SMOOTH_SCROLL_DURATION = 250
|
||||
private var smoothScrollDurationInitialized = false
|
||||
|
||||
/** Shared by [ReactScrollView] and [ReactHorizontalScrollView]. */
|
||||
@JvmStatic
|
||||
public fun <T> emitScrollEvent(scrollView: T, xVelocity: Float, yVelocity: Float) where
|
||||
T : HasScrollEventThrottle?,
|
||||
T : ViewGroup {
|
||||
emitScrollEvent(scrollView, ScrollEventType.SCROLL, xVelocity, yVelocity)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> emitScrollBeginDragEvent(scrollView: T) where
|
||||
T : HasScrollEventThrottle?,
|
||||
T : ViewGroup {
|
||||
emitScrollEvent(scrollView, ScrollEventType.BEGIN_DRAG)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> emitScrollEndDragEvent(scrollView: T, xVelocity: Float, yVelocity: Float) where
|
||||
T : HasScrollEventThrottle?,
|
||||
T : ViewGroup {
|
||||
emitScrollEvent(scrollView, ScrollEventType.END_DRAG, xVelocity, yVelocity)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> emitScrollMomentumBeginEvent(scrollView: T, xVelocity: Int, yVelocity: Int) where
|
||||
T : HasScrollEventThrottle?,
|
||||
T : ViewGroup {
|
||||
emitScrollEvent(
|
||||
scrollView, ScrollEventType.MOMENTUM_BEGIN, xVelocity.toFloat(), yVelocity.toFloat())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> emitScrollMomentumEndEvent(scrollView: T) where
|
||||
T : HasScrollEventThrottle?,
|
||||
T : ViewGroup {
|
||||
emitScrollEvent(scrollView, ScrollEventType.MOMENTUM_END)
|
||||
}
|
||||
|
||||
private fun <T> emitScrollEvent(scrollView: T, scrollEventType: ScrollEventType) where
|
||||
T : HasScrollEventThrottle?,
|
||||
T : ViewGroup {
|
||||
emitScrollEvent(scrollView, scrollEventType, 0f, 0f)
|
||||
}
|
||||
|
||||
private fun <T> emitScrollEvent(
|
||||
scrollView: T,
|
||||
scrollEventType: ScrollEventType,
|
||||
xVelocity: Float,
|
||||
yVelocity: Float
|
||||
) where T : HasScrollEventThrottle?, T : ViewGroup {
|
||||
val now = System.currentTimeMillis()
|
||||
// Throttle the scroll event if scrollEventThrottle is set to be equal or more than 17 ms.
|
||||
// We limit the delta to 17ms so that small throttles intended to enable 60fps updates will not
|
||||
// inadvertently filter out any scroll events.
|
||||
if (scrollView.scrollEventThrottle >= Math.max(17, now - scrollView.lastScrollDispatchTime)) {
|
||||
// Scroll events are throttled.
|
||||
return
|
||||
}
|
||||
val contentView = scrollView.getChildAt(0) ?: return
|
||||
for (scrollListener in scrollListeners) {
|
||||
scrollListener.onScroll(scrollView, scrollEventType, xVelocity, yVelocity)
|
||||
}
|
||||
val reactContext = scrollView.context as ReactContext
|
||||
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
||||
|
||||
// It's possible for the EventDispatcher to go away - for example,
|
||||
// if there's a crash initiated from JS and we tap on a ScrollView
|
||||
// around teardown of RN, this will cause a NPE. We can safely ignore
|
||||
// this since the crash is usually a red herring.
|
||||
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, scrollView.id)
|
||||
if (eventDispatcher != null) {
|
||||
eventDispatcher.dispatchEvent(
|
||||
ScrollEvent.obtain(
|
||||
surfaceId,
|
||||
scrollView.id,
|
||||
scrollEventType,
|
||||
scrollView.scrollX.toFloat(),
|
||||
scrollView.scrollY.toFloat(),
|
||||
xVelocity,
|
||||
yVelocity,
|
||||
contentView.width,
|
||||
contentView.height,
|
||||
scrollView.width,
|
||||
scrollView.height))
|
||||
scrollView.lastScrollDispatchTime = now
|
||||
}
|
||||
}
|
||||
|
||||
/** This is only for Java listeners. onLayout events emitted to JS are handled elsewhere. */
|
||||
@JvmStatic
|
||||
public fun emitLayoutEvent(scrollView: ViewGroup) {
|
||||
for (scrollListener in scrollListeners) {
|
||||
scrollListener.onLayout(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun parseOverScrollMode(jsOverScrollMode: String?): Int {
|
||||
return if (jsOverScrollMode == null || jsOverScrollMode == AUTO) {
|
||||
View.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
} else if (jsOverScrollMode == OVER_SCROLL_ALWAYS) {
|
||||
View.OVER_SCROLL_ALWAYS
|
||||
} else if (jsOverScrollMode == OVER_SCROLL_NEVER) {
|
||||
View.OVER_SCROLL_NEVER
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "wrong overScrollMode: $jsOverScrollMode")
|
||||
View.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun parseSnapToAlignment(alignment: String?): Int {
|
||||
return if (alignment == null) {
|
||||
SNAP_ALIGNMENT_DISABLED
|
||||
} else if ("start".equals(alignment, ignoreCase = true)) {
|
||||
SNAP_ALIGNMENT_START
|
||||
} else if ("center".equals(alignment, ignoreCase = true)) {
|
||||
SNAP_ALIGNMENT_CENTER
|
||||
} else if ("end" == alignment) {
|
||||
SNAP_ALIGNMENT_END
|
||||
} else {
|
||||
FLog.w(ReactConstants.TAG, "wrong snap alignment value: $alignment")
|
||||
SNAP_ALIGNMENT_DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun getDefaultScrollAnimationDuration(context: Context?): Int {
|
||||
if (!smoothScrollDurationInitialized) {
|
||||
smoothScrollDurationInitialized = true
|
||||
try {
|
||||
val overScrollerDurationGetter = OverScrollerDurationGetter(context)
|
||||
SMOOTH_SCROLL_DURATION = overScrollerDurationGetter.scrollAnimationDuration
|
||||
} catch (e: Throwable) {}
|
||||
}
|
||||
return SMOOTH_SCROLL_DURATION
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scroll listener.
|
||||
*
|
||||
* Note that you must keep a reference to this scroll listener because this class only keeps a
|
||||
* weak reference to it (to prevent memory leaks). This means that code like `
|
||||
* addScrollListener(new ScrollListener() {...})` won't work, you need to do this instead: `
|
||||
* mScrollListener = new ScrollListener() {...};
|
||||
* ReactScrollViewHelper.addScrollListener(mScrollListener); ` * instead.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun addScrollListener(listener: ScrollListener?) {
|
||||
if (listener != null) {
|
||||
scrollListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun removeScrollListener(listener: ScrollListener?) {
|
||||
if (listener != null) {
|
||||
scrollListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the given view to the location (x, y), with provided initial velocity. This method works
|
||||
* by calculate the "would be" initial velocity with internal friction to move to the point (x,
|
||||
* y), then apply that to the animator.
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun <T> smoothScrollTo(scrollView: T, x: Int, y: Int) where
|
||||
T : HasFlingAnimator?,
|
||||
T : HasScrollState?,
|
||||
T : HasStateWrapper?,
|
||||
T : ViewGroup {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(TAG, "smoothScrollTo[%d] x %d y %d", scrollView.id, x, y)
|
||||
}
|
||||
|
||||
// Register the listeners for the fling animator if there isn't any
|
||||
val flingAnimator = scrollView.getFlingAnimator()
|
||||
if (flingAnimator.listeners == null || flingAnimator.listeners.size == 0) {
|
||||
registerFlingAnimator<T>(scrollView)
|
||||
}
|
||||
val scrollState = scrollView.reactScrollViewScrollState
|
||||
scrollState.setFinalAnimatedPositionScroll(x, y)
|
||||
val scrollX = scrollView.scrollX
|
||||
val scrollY = scrollView.scrollY
|
||||
// Only one fling animator will be started. For the horizontal scroll view, scrollY will always
|
||||
// be the same to y. This is the same to the vertical scroll view.
|
||||
if (scrollX != x) {
|
||||
scrollView.startFlingAnimator(scrollX, x)
|
||||
}
|
||||
if (scrollY != y) {
|
||||
scrollView.startFlingAnimator(scrollY, y)
|
||||
}
|
||||
updateFabricScrollState<T>(scrollView, x, y)
|
||||
}
|
||||
|
||||
/** Get current position or position after current animation finishes, if any. */
|
||||
@JvmStatic
|
||||
public fun <T> getNextFlingStartValue(
|
||||
scrollView: T,
|
||||
currentValue: Int,
|
||||
postAnimationValue: Int,
|
||||
velocity: Int
|
||||
): Int where T : HasFlingAnimator?, T : HasScrollState?, T : ViewGroup {
|
||||
val scrollState = scrollView.reactScrollViewScrollState
|
||||
val velocityDirectionMask = if (velocity != 0) velocity / Math.abs(velocity) else 0
|
||||
val isMovingTowardsAnimatedValue =
|
||||
velocityDirectionMask * (postAnimationValue - currentValue) > 0
|
||||
|
||||
// When the fling animation is not finished, or it was canceled and now we are moving towards
|
||||
// the final animated value, we will return the final animated value. This is because follow up
|
||||
// animation should consider the "would be" animated location, so that previous quick small
|
||||
// scrolls are still working.
|
||||
return if (!scrollState.isFinished ||
|
||||
(scrollState.isCanceled && isMovingTowardsAnimatedValue)) {
|
||||
postAnimationValue
|
||||
} else {
|
||||
currentValue
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> updateFabricScrollState(scrollView: T) where
|
||||
T : HasFlingAnimator?,
|
||||
T : HasScrollState?,
|
||||
T : HasStateWrapper?,
|
||||
T : ViewGroup {
|
||||
updateFabricScrollState(scrollView, scrollView.scrollX, scrollView.scrollY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on any stabilized onScroll change to propagate content offset value to a Shadow Node.
|
||||
*/
|
||||
public fun <T> updateFabricScrollState(scrollView: T, scrollX: Int, scrollY: Int) where
|
||||
T : HasFlingAnimator?,
|
||||
T : HasScrollState?,
|
||||
T : HasStateWrapper?,
|
||||
T : ViewGroup {
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(
|
||||
TAG, "updateFabricScrollState[%d] scrollX %d scrollY %d", scrollView.id, scrollX, scrollY)
|
||||
}
|
||||
if (ViewUtil.getUIManagerType(scrollView.id) == UIManagerType.DEFAULT) {
|
||||
return
|
||||
}
|
||||
val scrollState = scrollView.reactScrollViewScrollState
|
||||
// Dedupe events to reduce JNI traffic
|
||||
if (scrollState.lastStateUpdateScroll.equals(scrollX, scrollY)) {
|
||||
return
|
||||
}
|
||||
scrollState.setLastStateUpdateScroll(scrollX, scrollY)
|
||||
forceUpdateState<T>(scrollView)
|
||||
return
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> forceUpdateState(scrollView: T) where
|
||||
T : HasFlingAnimator?,
|
||||
T : HasScrollState?,
|
||||
T : HasStateWrapper?,
|
||||
T : ViewGroup {
|
||||
val scrollState = scrollView.reactScrollViewScrollState
|
||||
val scrollAwayPaddingTop = scrollState.scrollAwayPaddingTop
|
||||
val scrollPos = scrollState.lastStateUpdateScroll
|
||||
val scrollX = scrollPos.x
|
||||
val scrollY = scrollPos.y
|
||||
val fabricScrollX: Int
|
||||
val layoutDirection = scrollState.layoutDirection
|
||||
fabricScrollX =
|
||||
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||
// getScrollX returns offset from left even when layout direction is RTL.
|
||||
// The following line calculates offset from right.
|
||||
val child = scrollView.getChildAt(0)
|
||||
val contentWidth = child?.width ?: 0
|
||||
-(contentWidth - scrollX - scrollView.width)
|
||||
} else {
|
||||
scrollX
|
||||
}
|
||||
if (DEBUG_MODE) {
|
||||
FLog.i(
|
||||
TAG,
|
||||
"updateFabricScrollState[%d] scrollX %d scrollY %d fabricScrollX",
|
||||
scrollView.id,
|
||||
scrollX,
|
||||
scrollY,
|
||||
fabricScrollX)
|
||||
}
|
||||
val stateWrapper = scrollView.stateWrapper
|
||||
if (stateWrapper != null) {
|
||||
val newStateData: WritableMap = WritableNativeMap()
|
||||
newStateData.putDouble(CONTENT_OFFSET_LEFT, toDIPFromPixel(scrollX.toFloat()).toDouble())
|
||||
newStateData.putDouble(CONTENT_OFFSET_TOP, toDIPFromPixel(scrollY.toFloat()).toDouble())
|
||||
newStateData.putDouble(
|
||||
SCROLL_AWAY_PADDING_TOP, toDIPFromPixel(scrollAwayPaddingTop.toFloat()).toDouble())
|
||||
stateWrapper.updateState(newStateData)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> updateStateOnScrollChanged(
|
||||
scrollView: T,
|
||||
xVelocity: Float,
|
||||
yVelocity: Float
|
||||
) where
|
||||
T : HasFlingAnimator?,
|
||||
T : HasScrollEventThrottle?,
|
||||
T : HasScrollState?,
|
||||
T : HasStateWrapper?,
|
||||
T : ViewGroup {
|
||||
// Race an UpdateState with every onScroll. This makes it more likely that, in Fabric,
|
||||
// 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.
|
||||
updateFabricScrollState(scrollView)
|
||||
emitScrollEvent(scrollView, xVelocity, yVelocity)
|
||||
}
|
||||
|
||||
public fun <T> registerFlingAnimator(scrollView: T) where
|
||||
T : HasFlingAnimator?,
|
||||
T : HasScrollState?,
|
||||
T : HasStateWrapper?,
|
||||
T : ViewGroup {
|
||||
scrollView
|
||||
.getFlingAnimator()
|
||||
.addListener(
|
||||
object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animator: Animator) {
|
||||
val scrollState = scrollView.reactScrollViewScrollState
|
||||
scrollState.isCanceled = false
|
||||
scrollState.isFinished = false
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animator: Animator) {
|
||||
scrollView.reactScrollViewScrollState.isFinished = true
|
||||
updateFabricScrollState<T>(scrollView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animator: Animator) {
|
||||
scrollView.reactScrollViewScrollState.isCanceled = true
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animator: Animator) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun <T> predictFinalScrollPosition(
|
||||
scrollView: T,
|
||||
velocityX: Int,
|
||||
velocityY: Int,
|
||||
maximumOffsetX: Int,
|
||||
maximumOffsetY: Int
|
||||
): Point where T : HasFlingAnimator?, T : HasScrollState?, T : ViewGroup {
|
||||
val scrollState = scrollView.reactScrollViewScrollState
|
||||
// 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
|
||||
// so we can predict where a fling would land and snap to nearby that point.
|
||||
val scroller = OverScroller(scrollView.context)
|
||||
scroller.setFriction(1.0f - scrollState.decelerationRate)
|
||||
|
||||
// predict where a fling would end up so we can scroll to the nearest snap offset
|
||||
val width =
|
||||
(scrollView.width -
|
||||
ViewCompat.getPaddingStart(scrollView) -
|
||||
ViewCompat.getPaddingEnd(scrollView))
|
||||
val height = scrollView.height - scrollView.paddingBottom - scrollView.paddingTop
|
||||
val finalAnimatedPositionScroll = scrollState.finalAnimatedPositionScroll
|
||||
scroller.fling(
|
||||
getNextFlingStartValue<T>(
|
||||
scrollView, scrollView.scrollX, finalAnimatedPositionScroll.x, velocityX), // startX
|
||||
getNextFlingStartValue<T>(
|
||||
scrollView, scrollView.scrollY, finalAnimatedPositionScroll.y, velocityY), // startY
|
||||
velocityX, // velocityX
|
||||
velocityY, // velocityY
|
||||
0, // minX
|
||||
maximumOffsetX, // maxX
|
||||
0, // minY
|
||||
maximumOffsetY, // maxY
|
||||
width / 2, // overX
|
||||
height / 2 // overY
|
||||
)
|
||||
return Point(scroller.finalX, scroller.finalY)
|
||||
}
|
||||
|
||||
public interface ScrollListener {
|
||||
public fun onScroll(
|
||||
scrollView: ViewGroup?,
|
||||
scrollEventType: ScrollEventType?,
|
||||
xVelocity: Float,
|
||||
yVelocity: Float
|
||||
)
|
||||
|
||||
public fun onLayout(scrollView: ViewGroup?)
|
||||
}
|
||||
|
||||
public interface HasStateWrapper {
|
||||
public val stateWrapper: StateWrapper?
|
||||
}
|
||||
|
||||
private class OverScrollerDurationGetter internal constructor(context: Context?) :
|
||||
OverScroller(context) {
|
||||
// This is the default in AOSP, hardcoded in OverScroller.java.
|
||||
private var currentScrollAnimationDuration = 250
|
||||
val scrollAnimationDuration: Int
|
||||
get() {
|
||||
// If startScroll is called without a duration, OverScroller will call `startScroll(x, y,
|
||||
// dx,
|
||||
// dy, duration)` with the default duration.
|
||||
super.startScroll(0, 0, 0, 0)
|
||||
return currentScrollAnimationDuration
|
||||
}
|
||||
|
||||
override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
|
||||
currentScrollAnimationDuration = duration
|
||||
}
|
||||
}
|
||||
|
||||
public class ReactScrollViewScrollState(
|
||||
/**
|
||||
* 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 val layoutDirection: Int
|
||||
) {
|
||||
|
||||
/** Get the position after current animation is finished */
|
||||
public val finalAnimatedPositionScroll: Point = Point()
|
||||
/** Get the padding on the top for nav bar */
|
||||
public var scrollAwayPaddingTop: Int = 0
|
||||
/** Get the Fabric state of last scroll position */
|
||||
public val lastStateUpdateScroll: Point = Point(-1, -1)
|
||||
/** Get true if the previous animation was canceled */
|
||||
public var isCanceled: Boolean = false
|
||||
/** Get true if previous animation was finished */
|
||||
public var isFinished: Boolean = true
|
||||
/** Get true if previous animation was finished */
|
||||
public var decelerationRate: Float = 0.985f
|
||||
|
||||
/** Set the final scroll position after scrolling animation is finished */
|
||||
public fun setFinalAnimatedPositionScroll(
|
||||
finalAnimatedPositionScrollX: Int,
|
||||
finalAnimatedPositionScrollY: Int
|
||||
): ReactScrollViewScrollState {
|
||||
finalAnimatedPositionScroll[finalAnimatedPositionScrollX] = finalAnimatedPositionScrollY
|
||||
return this
|
||||
}
|
||||
|
||||
/** Set the Fabric state of last scroll position */
|
||||
public fun setLastStateUpdateScroll(
|
||||
lastStateUpdateScrollX: Int,
|
||||
lastStateUpdateScrollY: Int
|
||||
): ReactScrollViewScrollState {
|
||||
lastStateUpdateScroll[lastStateUpdateScrollX] = lastStateUpdateScrollY
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
public interface HasScrollState {
|
||||
/** Get the scroll state for the current ScrollView */
|
||||
public val reactScrollViewScrollState: ReactScrollViewScrollState
|
||||
}
|
||||
|
||||
public interface HasFlingAnimator {
|
||||
/**
|
||||
* Start the fling animator that the ScrollView has to go from the start position to end
|
||||
* position.
|
||||
*/
|
||||
public fun startFlingAnimator(start: Int, end: Int)
|
||||
|
||||
/** Get the fling animator that is reused for the ScrollView to handle fling animation. */
|
||||
public fun getFlingAnimator(): ValueAnimator
|
||||
|
||||
/** Get the fling distance with current velocity for prediction */
|
||||
public fun getFlingExtrapolatedDistance(velocity: Int): Int
|
||||
}
|
||||
|
||||
public interface HasScrollEventThrottle {
|
||||
/** Get the scroll event throttle in ms. */
|
||||
/**
|
||||
* Set the scroll event throttle in ms. This number is used to throttle the scroll events. The
|
||||
* default value is zero, which means the scroll events are sent with no throttle.
|
||||
*/
|
||||
public var scrollEventThrottle: Int
|
||||
/** Get the scroll view dispatch time for throttling */
|
||||
/** Set the scroll view's last dispatch time for throttling */
|
||||
public var lastScrollDispatchTime: Long
|
||||
}
|
||||
|
||||
public interface HasSmoothScroll {
|
||||
public fun reactSmoothScrollTo(x: Int, y: Int)
|
||||
|
||||
public fun scrollToPreservingMomentum(x: Int, y: Int)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user