diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index b6c5c7d47f5..7d27ba44c69 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -476,6 +476,7 @@ public class com/facebook/react/animated/NativeAnimatedModule : com/facebook/fbr public fun stopAnimation (D)V public fun stopListeningToAnimatedNodeValue (D)V public fun updateAnimatedNodeConfig (DLcom/facebook/react/bridge/ReadableMap;)V + public fun userDrivenScrollEnded (I)V public fun willDispatchViewUpdates (Lcom/facebook/react/bridge/UIManager;)V public fun willMountItems (Lcom/facebook/react/bridge/UIManager;)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java index bbe82a56ed9..572eacffe4a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UIManager; import com.facebook.react.bridge.UIManagerListener; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; @@ -38,6 +39,7 @@ import com.facebook.react.uimanager.common.ViewUtil; import java.util.ArrayList; import java.util.List; import java.util.Queue; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicReference; @@ -261,6 +263,43 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec }; } + /** + * This method is used to notify the JS side that the user has stopped scrolling. With natively + * driven animation, we might have to force a resync between the Shadow Tree and the Native Tree. + * This is because with natively driven animation, the Shadow Tree is bypassed and it can have + * stale information on the layout of the native views. This method takes care of verifying if + * there are some views listening to the native driven animation and it triggers the resynch. + * + * @param viewTag The tag of the scroll view that has stopped scrolling + */ + public void userDrivenScrollEnded(int viewTag) { + // ask to the Node Manager for all the native nodes listening to OnScroll event + NativeAnimatedNodesManager nodeManager = mNodesManager.get(); + if (nodeManager == null) { + return; + } + + Set tags = nodeManager.getTagsOfConnectedNodes(viewTag, "topScrollEnded"); + + if (tags.isEmpty()) { + return; + } + + WritableArray tagsArray = Arguments.createArray(); + for (Integer tag : tags) { + tagsArray.pushInt(tag); + } + + // emit the event to JS to resync the trees + WritableMap onAnimationEndedData = Arguments.createMap(); + onAnimationEndedData.putArray("tags", tagsArray); + + ReactApplicationContext reactApplicationContext = getReactApplicationContextIfActiveOrWarn(); + if (reactApplicationContext != null) { + reactApplicationContext.emitDeviceEvent("onUserDrivenAnimationEnded", onAnimationEndedData); + } + } + @Override public void initialize() { super.initialize(); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 0f7ae54eb03..dde5b41b6de 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -32,10 +32,12 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.EventDispatcherListener; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Queue; +import java.util.Set; /** * This is the main class that coordinates how native animated JS implementation drives UI changes. @@ -545,6 +547,11 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { EventAnimationDriver eventDriver = new EventAnimationDriver(eventName, viewTag, pathList, (ValueAnimatedNode) node); mEventDrivers.add(eventDriver); + + if (eventName.equals("topScroll")) { + // Handle the custom topScrollEnded event sent by the ScrollViews when the user stops dragging + addAnimatedEventToView(viewTag, "topScrollEnded", eventMapping); + } } @UiThread @@ -689,6 +696,28 @@ public class NativeAnimatedNodesManager implements EventDispatcherListener { } } + Set getTagsOfConnectedNodes(int tag, String eventName) { + Set tags = new HashSet<>(); + + // Filter only relevant animation drivers + ListIterator it = mEventDrivers.listIterator(); + while (it.hasNext()) { + EventAnimationDriver driver = it.next(); + if (driver != null) { + if (eventName.equals(driver.mEventName) && tag == driver.mViewTag) { + tags.add(driver.mViewTag); + if (driver.mValueNode != null && driver.mValueNode.mChildren != null) { + for (AnimatedNode node : driver.mValueNode.mChildren) { + tags.add(node.mTag); + } + } + } + } + } + + return tags; + } + @UiThread private void updateNodes(List nodes) { int activeNodesCount = 0; 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 5a069a809ca..7b99234f7aa 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 @@ -33,6 +33,8 @@ import androidx.core.view.ViewCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.animated.NativeAnimatedModule; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; @@ -927,6 +929,15 @@ public class ReactHorizontalScrollView extends HorizontalScrollView if (mSendMomentumEvents) { ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactHorizontalScrollView.this); } + + ReactContext context = (ReactContext) getContext(); + if (context != null) { + NativeAnimatedModule nativeAnimated = + context.getNativeModule(NativeAnimatedModule.class); + if (nativeAnimated != null) { + nativeAnimated.userDrivenScrollEnded(ReactHorizontalScrollView.this.getId()); + } + } disableFpsListener(); } else { if (mPagingEnabled && !mSnappingToPage) { 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 7e9268e69b0..ca3c4b88043 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 @@ -35,6 +35,8 @@ import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Nullsafe; import com.facebook.react.R; +import com.facebook.react.animated.NativeAnimatedModule; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.MeasureSpecAssertions; @@ -701,6 +703,14 @@ public class ReactScrollView extends ScrollView if (mSendMomentumEvents) { ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactScrollView.this); } + ReactContext context = (ReactContext) getContext(); + if (context != null) { + NativeAnimatedModule nativeAnimated = + context.getNativeModule(NativeAnimatedModule.class); + if (nativeAnimated != null) { + nativeAnimated.userDrivenScrollEnded(ReactScrollView.this.getId()); + } + } disableFpsListener(); } else { if (mPagingEnabled && !mSnappingToPage) {