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
This commit is contained in:
Riccardo Cipolleschi
2024-07-17 09:22:39 -07:00
committed by Facebook GitHub Bot
parent fd748ae84c
commit 394aae1c1c
5 changed files with 90 additions and 0 deletions
@@ -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
}
@@ -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<Integer> 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();
@@ -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<Integer> getTagsOfConnectedNodes(int tag, String eventName) {
Set<Integer> tags = new HashSet<>();
// Filter only relevant animation drivers
ListIterator<EventAnimationDriver> 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<AnimatedNode> nodes) {
int activeNodesCount = 0;
@@ -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) {
@@ -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) {