From aa85da33a51eb102fdcfacad76002e826dd2ba14 Mon Sep 17 00:00:00 2001 From: Nick Lefever Date: Tue, 7 Oct 2025 19:20:15 -0700 Subject: [PATCH] Support loading and assigning scroll state from scroll view helper (#53999) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53999 Update the `ScrollViewHelper` class to support reading the scroll state from the provided `stateWrapper` and assigning it to the view implementing the `HasScrollState` interface. This will be used in a future diff to implement scroll state loading on the scroll view component in the scroll state setter function. This enables having one implementation of the state loading for all variants of the scroll view (vertical and horizontal scroll views). Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D83247018 fbshipit-source-id: 44f10edab2341981b85d6ab8b83c7bea96c1e09d --- .../ReactAndroid/api/ReactAndroid.api | 19 ++++++ .../scroll/ReactHorizontalScrollView.java | 5 ++ .../react/views/scroll/ReactScrollView.java | 5 ++ .../views/scroll/ReactScrollViewHelper.kt | 61 ++++++++++++++----- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 0d2d365d92b..4367487af7f 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5636,6 +5636,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun setOverflowInset (IIII)V public fun setPagingEnabled (Z)V public fun setPointerEvents (Lcom/facebook/react/uimanager/PointerEvents;)V + public fun setReactScrollViewScrollState (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;)V public fun setRemoveClippedSubviews (Z)V public fun setScrollEnabled (Z)V public fun setScrollEventThrottle (I)V @@ -5775,6 +5776,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public fun setOverflowInset (IIII)V public fun setPagingEnabled (Z)V public fun setPointerEvents (Lcom/facebook/react/uimanager/PointerEvents;)V + public fun setReactScrollViewScrollState (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;)V public fun setRemoveClippedSubviews (Z)V public fun setScrollAwayTopPaddingEnabledUnstable (I)V public fun setScrollEnabled (Z)V @@ -5879,6 +5881,7 @@ public abstract interface class com/facebook/react/views/scroll/ReactScrollViewH public abstract interface class com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState { public abstract fun getReactScrollViewScrollState ()Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState; + public abstract fun setReactScrollViewScrollState (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;)V } public abstract interface class com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll { @@ -5896,18 +5899,34 @@ public abstract interface class com/facebook/react/views/scroll/ReactScrollViewH public final class com/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState { public fun ()V + public fun (Landroid/graphics/Point;ILandroid/graphics/Point;ZZFZ)V + public synthetic fun (Landroid/graphics/Point;ILandroid/graphics/Point;ZZFZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Landroid/graphics/Point; + public final fun component2 ()I + public final fun component3 ()Landroid/graphics/Point; + public final fun component4 ()Z + public final fun component5 ()Z + public final fun component6 ()F + public final fun component7 ()Z + public final fun copy (Landroid/graphics/Point;ILandroid/graphics/Point;ZZFZ)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState; + public static synthetic fun copy$default (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState;Landroid/graphics/Point;ILandroid/graphics/Point;ZZFZILjava/lang/Object;)Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ReactScrollViewScrollState; + public fun equals (Ljava/lang/Object;)Z public final fun getDecelerationRate ()F public final fun getFinalAnimatedPositionScroll ()Landroid/graphics/Point; public final fun getLastStateUpdateScroll ()Landroid/graphics/Point; public final fun getScrollAwayPaddingTop ()I + public fun hashCode ()I public final fun isCanceled ()Z public final fun isFinished ()Z + public final fun isUpdatedByScroll ()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 final fun setUpdatedByScroll (Z)V + public fun toString ()Ljava/lang/String; } public abstract interface class com/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 63491c9d1c5..10a701a73f0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -1649,6 +1649,11 @@ public class ReactHorizontalScrollView extends HorizontalScrollView mStateWrapper = stateWrapper; } + @Override + public void setReactScrollViewScrollState(ReactScrollViewScrollState scrollState) { + mReactScrollViewScrollState = scrollState; + } + @Override public ReactScrollViewScrollState getReactScrollViewScrollState() { return mReactScrollViewScrollState; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 69b5347f04c..d6951ed8436 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -1467,6 +1467,11 @@ public class ReactScrollView extends ScrollView ReactScrollViewHelper.forceUpdateState(this); } + @Override + public void setReactScrollViewScrollState(ReactScrollViewScrollState scrollState) { + mReactScrollViewScrollState = scrollState; + } + @Override public ReactScrollViewScrollState getReactScrollViewScrollState() { return mReactScrollViewScrollState; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt index 4621ccaf8ae..7e86543794f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt @@ -26,6 +26,7 @@ import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.common.ReactConstants import com.facebook.react.fabric.FabricUIManager import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel +import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP import com.facebook.react.uimanager.ReactClippingViewGroup import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.UIManagerHelper @@ -361,6 +362,10 @@ public object ReactScrollViewHelper { return } val scrollState = scrollView.reactScrollViewScrollState + + // User driven scrolling should disable scroll state updates coming from Fabric + scrollState.isUpdatedByScroll = true + // Dedupe events to reduce JNI traffic if (scrollState.lastStateUpdateScroll.equals(scrollX, scrollY)) { return @@ -399,6 +404,29 @@ public object ReactScrollViewHelper { } } + @JvmStatic + internal fun loadFabricScrollState(scrollView: T, stateWrapper: StateWrapper) + where T : HasScrollState?, T : HasStateWrapper?, T : ViewGroup { + if (scrollView.reactScrollViewScrollState.isUpdatedByScroll) { + return + } + + val stateData = stateWrapper.stateData + if (stateData == null) { + return + } + + // Assign the data loaded from the shadow node state + val scrollX = toPixelFromDIP(stateData.getDouble(CONTENT_OFFSET_LEFT)).toInt() + val scrollY = toPixelFromDIP(stateData.getDouble(CONTENT_OFFSET_TOP)).toInt() + val scrollAwayPaddingTop = toPixelFromDIP(stateData.getDouble(SCROLL_AWAY_PADDING_TOP)).toInt() + + val scrollState = + scrollView.reactScrollViewScrollState.copy(scrollAwayPaddingTop = scrollAwayPaddingTop) + scrollState.setLastStateUpdateScroll(scrollX, scrollY) + scrollView.reactScrollViewScrollState = scrollState + } + @JvmStatic public fun updateStateOnScrollChanged(scrollView: T, xVelocity: Float, yVelocity: Float) where @@ -596,21 +624,22 @@ public object ReactScrollViewHelper { } } - public class ReactScrollViewScrollState { - - /** 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 - + public data class ReactScrollViewScrollState( + /** Get the position after current animation is finished */ + val finalAnimatedPositionScroll: Point = Point(), + /** Get the padding on the top for nav bar */ + var scrollAwayPaddingTop: Int = 0, + /** Get the Fabric state of last scroll position */ + val lastStateUpdateScroll: Point = Point(-1, -1), + /** Get true if the previous animation was canceled */ + var isCanceled: Boolean = false, + /** Get true if previous animation was finished */ + var isFinished: Boolean = true, + /** Get true if previous animation was finished */ + var decelerationRate: Float = 0.985f, + /** Get true if the component submitted the state through user scrolling */ + var isUpdatedByScroll: Boolean = false, + ) { /** Set the final scroll position after scrolling animation is finished */ public fun setFinalAnimatedPositionScroll( finalAnimatedPositionScrollX: Int, @@ -632,7 +661,7 @@ public object ReactScrollViewHelper { public interface HasScrollState { /** Get the scroll state for the current ScrollView */ - public val reactScrollViewScrollState: ReactScrollViewScrollState + public var reactScrollViewScrollState: ReactScrollViewScrollState } public interface HasFlingAnimator {