From 394aae1c1cc354eaefb762455e2bbeeec0503038 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Wed, 17 Jul 2024 09:22:39 -0700 Subject: [PATCH] Make the ScrollView connect with the TurboAnimatedModule to fix pressable and animated (#45457) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45457 With this change, we are implementing in Android a similar logic that we implemented in iOS. 1. When the user stops dragging a scroll view, it tells native animated modle that a scroll has finished 2. NativeAnimated module asks to the NativeAnimatedNodesModule if there are native node listening to the scroll 3. In case they are, it emits an event to JS 4. JS listen to the events and resync the Shadow Tree and the Native Tree (this implemented in a previous change) ## Changelog [Android][Fixed] - Sync the Shadow Tree and the Native Tree with Native animation when scroll is driving the animation Reviewed By: sammy-SC Differential Revision: D59756577 fbshipit-source-id: e558557b477f4da9da1f89fb31ba86d0ea1390a3 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../react/animated/NativeAnimatedModule.java | 39 +++++++++++++++++++ .../animated/NativeAnimatedNodesManager.java | 29 ++++++++++++++ .../scroll/ReactHorizontalScrollView.java | 11 ++++++ .../react/views/scroll/ReactScrollView.java | 10 +++++ 5 files changed, 90 insertions(+) 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) {