mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Convert NativeAnimatedNodesManager to kotlin
Summary: ## Changelog: [Android] [Changed] - Convert NativeAnimatedNodesManager to kotlin Reviewed By: alanleedev Differential Revision: D72657697 fbshipit-source-id: 36180d0906f6ef621b8b667442b61642f9ccc5d1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
02bf24b1d8
commit
bfb274c244
@@ -439,6 +439,14 @@ public abstract interface class com/facebook/react/ViewManagerOnDemandReactPacka
|
||||
public abstract fun getViewManagerNames (Lcom/facebook/react/bridge/ReactApplicationContext;)Ljava/util/Collection;
|
||||
}
|
||||
|
||||
public abstract class com/facebook/react/animated/AnimatedNode {
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public abstract interface class com/facebook/react/animated/AnimatedNodeValueListener {
|
||||
public abstract fun onValueUpdate (D)V
|
||||
}
|
||||
|
||||
public final class com/facebook/react/animated/NativeAnimatedModule : com/facebook/fbreact/specs/NativeAnimatedModuleSpec, com/facebook/react/bridge/LifecycleEventListener, com/facebook/react/bridge/UIManagerListener {
|
||||
public static final field ANIMATED_MODULE_DEBUG Z
|
||||
public static final field Companion Lcom/facebook/react/animated/NativeAnimatedModule$Companion;
|
||||
@@ -485,32 +493,32 @@ public final class com/facebook/react/animated/NativeAnimatedModule : com/facebo
|
||||
public final class com/facebook/react/animated/NativeAnimatedModule$Companion {
|
||||
}
|
||||
|
||||
public class com/facebook/react/animated/NativeAnimatedNodesManager : com/facebook/react/uimanager/events/EventDispatcherListener {
|
||||
public final class com/facebook/react/animated/NativeAnimatedNodesManager : com/facebook/react/uimanager/events/EventDispatcherListener {
|
||||
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
|
||||
public fun addAnimatedEventToView (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V
|
||||
public fun connectAnimatedNodeToView (II)V
|
||||
public fun connectAnimatedNodes (II)V
|
||||
public fun createAnimatedNode (ILcom/facebook/react/bridge/ReadableMap;)V
|
||||
public fun disconnectAnimatedNodeFromView (II)V
|
||||
public fun disconnectAnimatedNodes (II)V
|
||||
public fun dropAnimatedNode (I)V
|
||||
public fun extractAnimatedNodeOffset (I)V
|
||||
public fun flattenAnimatedNodeOffset (I)V
|
||||
public fun getNodeById (I)Lcom/facebook/react/animated/AnimatedNode;
|
||||
public fun getValue (ILcom/facebook/react/bridge/Callback;)V
|
||||
public fun hasActiveAnimations ()Z
|
||||
public fun initializeEventListenerForUIManagerType (I)V
|
||||
public final fun addAnimatedEventToView (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V
|
||||
public final fun connectAnimatedNodeToView (II)V
|
||||
public final fun connectAnimatedNodes (II)V
|
||||
public final fun createAnimatedNode (ILcom/facebook/react/bridge/ReadableMap;)V
|
||||
public final fun disconnectAnimatedNodeFromView (II)V
|
||||
public final fun disconnectAnimatedNodes (II)V
|
||||
public final fun dropAnimatedNode (I)V
|
||||
public final fun extractAnimatedNodeOffset (I)V
|
||||
public final fun flattenAnimatedNodeOffset (I)V
|
||||
public final fun getNodeById (I)Lcom/facebook/react/animated/AnimatedNode;
|
||||
public final fun getValue (ILcom/facebook/react/bridge/Callback;)V
|
||||
public final fun hasActiveAnimations ()Z
|
||||
public final fun initializeEventListenerForUIManagerType (I)V
|
||||
public fun onEventDispatch (Lcom/facebook/react/uimanager/events/Event;)V
|
||||
public fun removeAnimatedEventFromView (ILjava/lang/String;I)V
|
||||
public fun restoreDefaultValues (I)V
|
||||
public fun runUpdates (J)V
|
||||
public fun setAnimatedNodeOffset (ID)V
|
||||
public fun setAnimatedNodeValue (ID)V
|
||||
public fun startAnimatingNode (IILcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Callback;)V
|
||||
public fun startListeningToAnimatedNodeValue (ILcom/facebook/react/animated/AnimatedNodeValueListener;)V
|
||||
public fun stopAnimation (I)V
|
||||
public fun stopListeningToAnimatedNodeValue (I)V
|
||||
public fun updateAnimatedNodeConfig (ILcom/facebook/react/bridge/ReadableMap;)V
|
||||
public final fun removeAnimatedEventFromView (ILjava/lang/String;I)V
|
||||
public final fun restoreDefaultValues (I)V
|
||||
public final fun runUpdates (J)V
|
||||
public final fun setAnimatedNodeOffset (ID)V
|
||||
public final fun setAnimatedNodeValue (ID)V
|
||||
public final fun startAnimatingNode (IILcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Callback;)V
|
||||
public final fun startListeningToAnimatedNodeValue (ILcom/facebook/react/animated/AnimatedNodeValueListener;)V
|
||||
public final fun stopAnimation (I)V
|
||||
public final fun stopListeningToAnimatedNodeValue (I)V
|
||||
public final fun updateAnimatedNodeConfig (ILcom/facebook/react/bridge/ReadableMap;)V
|
||||
}
|
||||
|
||||
public abstract interface class com/facebook/react/bridge/ActivityEventListener {
|
||||
|
||||
+11
-11
@@ -10,11 +10,11 @@ package com.facebook.react.animated
|
||||
import java.util.ArrayList
|
||||
|
||||
/** Base class for all Animated.js library node types that can be created on the "native" side. */
|
||||
internal abstract class AnimatedNode {
|
||||
public abstract class AnimatedNode {
|
||||
|
||||
companion object {
|
||||
const val INITIAL_BFS_COLOR: Int = 0
|
||||
const val DEFAULT_ANIMATED_NODE_CHILD_COUNT: Int = 1
|
||||
internal companion object {
|
||||
internal const val INITIAL_BFS_COLOR: Int = 0
|
||||
internal const val DEFAULT_ANIMATED_NODE_CHILD_COUNT: Int = 1
|
||||
}
|
||||
|
||||
// TODO: T196787278 Reduce the visibility of these fields to package once we have
|
||||
@@ -27,7 +27,7 @@ internal abstract class AnimatedNode {
|
||||
@JvmField internal var BFSColor: Int = INITIAL_BFS_COLOR
|
||||
@JvmField internal var tag: Int = -1
|
||||
|
||||
fun addChild(child: AnimatedNode): Unit {
|
||||
internal fun addChild(child: AnimatedNode): Unit {
|
||||
val currentChildren =
|
||||
children
|
||||
?: ArrayList<AnimatedNode>(DEFAULT_ANIMATED_NODE_CHILD_COUNT).also { children = it }
|
||||
@@ -36,7 +36,7 @@ internal abstract class AnimatedNode {
|
||||
child.onAttachedToNode(this)
|
||||
}
|
||||
|
||||
fun removeChild(child: AnimatedNode): Unit {
|
||||
internal fun removeChild(child: AnimatedNode): Unit {
|
||||
val currentChildren = children ?: return
|
||||
child.onDetachedFromNode(this)
|
||||
currentChildren.remove(child)
|
||||
@@ -48,24 +48,24 @@ internal abstract class AnimatedNode {
|
||||
* is important to also override [onDetachedFromNode] to clear that reference once current node
|
||||
* gets detached.
|
||||
*/
|
||||
open fun onAttachedToNode(parent: AnimatedNode): Unit = Unit
|
||||
internal open fun onAttachedToNode(parent: AnimatedNode): Unit = Unit
|
||||
|
||||
/** See [onAttachedToNode] */
|
||||
open fun onDetachedFromNode(parent: AnimatedNode): Unit = Unit
|
||||
internal open fun onDetachedFromNode(parent: AnimatedNode): Unit = Unit
|
||||
|
||||
/**
|
||||
* This method will be run on each node at most once every repetition of the animation loop. It
|
||||
* will be executed on a node only when all the node's parent has already been updated. Therefore
|
||||
* it can be used to calculate node's value.
|
||||
*/
|
||||
open fun update(): Unit = Unit
|
||||
internal open fun update(): Unit = Unit
|
||||
|
||||
/**
|
||||
* Pretty-printer for the AnimatedNode. Only called in production pre-crash for debug diagnostics.
|
||||
*/
|
||||
abstract fun prettyPrint(): String
|
||||
internal abstract fun prettyPrint(): String
|
||||
|
||||
fun prettyPrintWithChildren(): String {
|
||||
internal fun prettyPrintWithChildren(): String {
|
||||
|
||||
val currentChildren = children?.joinToString(" ")
|
||||
return prettyPrint() +
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@
|
||||
package com.facebook.react.animated
|
||||
|
||||
/** Interface used to listen to [ValueAnimatedNode] updates. */
|
||||
internal fun interface AnimatedNodeValueListener {
|
||||
fun onValueUpdate(value: Double)
|
||||
public fun interface AnimatedNodeValueListener {
|
||||
public fun onValueUpdate(value: Double)
|
||||
}
|
||||
|
||||
-900
@@ -1,900 +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.animated;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.JSApplicationCausedNativeException;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactNoCrashSoftException;
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.UIManagerHelper;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.events.Event;
|
||||
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.
|
||||
*
|
||||
* <p>It implements a management interface for animated nodes graph as well as implements a graph
|
||||
* traversal algorithm that is run for each animation frame.
|
||||
*
|
||||
* <p>For each animation frame we visit animated nodes that might've been updated as well as their
|
||||
* children that may use parent's values to update themselves. At the end of the traversal algorithm
|
||||
* we expect to reach a special type of the node: PropsAnimatedNode that is then responsible for
|
||||
* calculating property map which can be sent to native view hierarchy to update the view.
|
||||
*
|
||||
* <p>IMPORTANT: This class should be accessed only from the UI Thread
|
||||
*/
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public class NativeAnimatedNodesManager implements EventDispatcherListener {
|
||||
|
||||
private static final String TAG = "NativeAnimatedNodesManager";
|
||||
|
||||
private final SparseArray<AnimatedNode> mAnimatedNodes = new SparseArray<>();
|
||||
private final SparseArray<AnimationDriver> mActiveAnimations = new SparseArray<>();
|
||||
private final SparseArray<AnimatedNode> mUpdatedNodes = new SparseArray<>();
|
||||
// List of event animation drivers for an event on view.
|
||||
// There may be multiple drivers for the same event and view.
|
||||
private final List<EventAnimationDriver> mEventDrivers = new ArrayList<>();
|
||||
private final ReactApplicationContext mReactApplicationContext;
|
||||
private int mAnimatedGraphBFSColor = 0;
|
||||
// Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`.
|
||||
private final List<AnimatedNode> mRunUpdateNodeList = new LinkedList<>();
|
||||
|
||||
private boolean mEventListenerInitializedForFabric = false;
|
||||
private boolean mEventListenerInitializedForNonFabric = false;
|
||||
|
||||
private boolean mWarnedAboutGraphTraversal = false;
|
||||
|
||||
public NativeAnimatedNodesManager(ReactApplicationContext reactApplicationContext) {
|
||||
mReactApplicationContext = reactApplicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize event listeners for Fabric UIManager or non-Fabric UIManager, exactly once. Once
|
||||
* Fabric is the only UIManager, this logic can be simplified. This is expected to only be called
|
||||
* from the native module thread.
|
||||
*
|
||||
* @param uiManagerType
|
||||
*/
|
||||
public void initializeEventListenerForUIManagerType(@UIManagerType final int uiManagerType) {
|
||||
if (uiManagerType == UIManagerType.FABRIC
|
||||
? mEventListenerInitializedForFabric
|
||||
: mEventListenerInitializedForNonFabric) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIManager uiManager = UIManagerHelper.getUIManager(mReactApplicationContext, uiManagerType);
|
||||
if (uiManager != null) {
|
||||
EventDispatcher eventDispatcher = (EventDispatcher) uiManager.getEventDispatcher();
|
||||
eventDispatcher.addListener(this);
|
||||
if (uiManagerType == UIManagerType.FABRIC) {
|
||||
mEventListenerInitializedForFabric = true;
|
||||
} else {
|
||||
mEventListenerInitializedForNonFabric = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AnimatedNode getNodeById(int id) {
|
||||
return mAnimatedNodes.get(id);
|
||||
}
|
||||
|
||||
public boolean hasActiveAnimations() {
|
||||
return mActiveAnimations.size() > 0 || mUpdatedNodes.size() > 0;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void createAnimatedNode(int tag, ReadableMap config) {
|
||||
if (mAnimatedNodes.get(tag) != null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"createAnimatedNode: Animated node [" + tag + "] already exists");
|
||||
}
|
||||
String type = config.getString("type");
|
||||
final AnimatedNode node;
|
||||
if ("style".equals(type)) {
|
||||
node = new StyleAnimatedNode(config, this);
|
||||
} else if ("value".equals(type)) {
|
||||
node = new ValueAnimatedNode(config);
|
||||
} else if ("color".equals(type)) {
|
||||
node = new ColorAnimatedNode(config, this, mReactApplicationContext);
|
||||
} else if ("props".equals(type)) {
|
||||
node = new PropsAnimatedNode(config, this);
|
||||
} else if ("interpolation".equals(type)) {
|
||||
node = new InterpolationAnimatedNode(config);
|
||||
} else if ("addition".equals(type)) {
|
||||
node = new AdditionAnimatedNode(config, this);
|
||||
} else if ("subtraction".equals(type)) {
|
||||
node = new SubtractionAnimatedNode(config, this);
|
||||
} else if ("division".equals(type)) {
|
||||
node = new DivisionAnimatedNode(config, this);
|
||||
} else if ("multiplication".equals(type)) {
|
||||
node = new MultiplicationAnimatedNode(config, this);
|
||||
} else if ("modulus".equals(type)) {
|
||||
node = new ModulusAnimatedNode(config, this);
|
||||
} else if ("diffclamp".equals(type)) {
|
||||
node = new DiffClampAnimatedNode(config, this);
|
||||
} else if ("transform".equals(type)) {
|
||||
node = new TransformAnimatedNode(config, this);
|
||||
} else if ("tracking".equals(type)) {
|
||||
node = new TrackingAnimatedNode(config, this);
|
||||
} else if ("object".equals(type)) {
|
||||
node = new ObjectAnimatedNode(config, this);
|
||||
} else {
|
||||
throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type);
|
||||
}
|
||||
node.tag = tag;
|
||||
mAnimatedNodes.put(tag, node);
|
||||
mUpdatedNodes.put(tag, node);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void updateAnimatedNodeConfig(int tag, ReadableMap config) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"updateAnimatedNode: Animated node [" + tag + "] does not exist");
|
||||
}
|
||||
|
||||
if (node instanceof AnimatedNodeWithUpdateableConfig) {
|
||||
stopAnimationsForNode(node);
|
||||
((AnimatedNodeWithUpdateableConfig) node).onUpdateConfig(config);
|
||||
mUpdatedNodes.put(tag, node);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void dropAnimatedNode(int tag) {
|
||||
mAnimatedNodes.remove(tag);
|
||||
mUpdatedNodes.remove(tag);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void startListeningToAnimatedNodeValue(int tag, AnimatedNodeValueListener listener) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"startListeningToAnimatedNodeValue: Animated node ["
|
||||
+ tag
|
||||
+ "] does not exist, or is not a 'value' node");
|
||||
}
|
||||
((ValueAnimatedNode) node).setValueListener(listener);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void stopListeningToAnimatedNodeValue(int tag) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"startListeningToAnimatedNodeValue: Animated node ["
|
||||
+ tag
|
||||
+ "] does not exist, or is not a 'value' node");
|
||||
}
|
||||
((ValueAnimatedNode) node).setValueListener(null);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setAnimatedNodeValue(int tag, double value) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"setAnimatedNodeValue: Animated node ["
|
||||
+ tag
|
||||
+ "] does not exist, or is not a 'value' node");
|
||||
}
|
||||
stopAnimationsForNode(node);
|
||||
((ValueAnimatedNode) node).nodeValue = value;
|
||||
mUpdatedNodes.put(tag, node);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setAnimatedNodeOffset(int tag, double offset) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"setAnimatedNodeOffset: Animated node ["
|
||||
+ tag
|
||||
+ "] does not exist, or is not a 'value' node");
|
||||
}
|
||||
((ValueAnimatedNode) node).offset = offset;
|
||||
mUpdatedNodes.put(tag, node);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void flattenAnimatedNodeOffset(int tag) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"flattenAnimatedNodeOffset: Animated node ["
|
||||
+ tag
|
||||
+ "] does not exist, or is not a 'value' node");
|
||||
}
|
||||
((ValueAnimatedNode) node).flattenOffset();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void extractAnimatedNodeOffset(int tag) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"extractAnimatedNodeOffset: Animated node ["
|
||||
+ tag
|
||||
+ "] does not exist, or is not a 'value' node");
|
||||
}
|
||||
((ValueAnimatedNode) node).extractOffset();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void startAnimatingNode(
|
||||
int animationId,
|
||||
int animatedNodeTag,
|
||||
ReadableMap animationConfig,
|
||||
@Nullable Callback endCallback) {
|
||||
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
|
||||
if (node == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"startAnimatingNode: Animated node [" + animatedNodeTag + "] does not exist");
|
||||
}
|
||||
if (!(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"startAnimatingNode: Animated node ["
|
||||
+ animatedNodeTag
|
||||
+ "] should be of type "
|
||||
+ ValueAnimatedNode.class.getName());
|
||||
}
|
||||
|
||||
final AnimationDriver existingDriver = mActiveAnimations.get(animationId);
|
||||
if (existingDriver != null) {
|
||||
// animation with the given ID is already running, we need to update its configuration instead
|
||||
// of spawning a new one
|
||||
existingDriver.resetConfig(animationConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
String type = animationConfig.getString("type");
|
||||
final AnimationDriver animation;
|
||||
if ("frames".equals(type)) {
|
||||
animation = new FrameBasedAnimationDriver(animationConfig);
|
||||
} else if ("spring".equals(type)) {
|
||||
animation = new SpringAnimation(animationConfig);
|
||||
} else if ("decay".equals(type)) {
|
||||
animation = new DecayAnimation(animationConfig);
|
||||
} else {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"startAnimatingNode: Unsupported animation type [" + animatedNodeTag + "]: " + type);
|
||||
}
|
||||
animation.id = animationId;
|
||||
animation.endCallback = endCallback;
|
||||
animation.animatedValue = (ValueAnimatedNode) node;
|
||||
mActiveAnimations.put(animationId, animation);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void stopAnimationsForNode(AnimatedNode animatedNode) {
|
||||
// in most of the cases there should never be more than a few active animations running at the
|
||||
// same time. Therefore it does not make much sense to create an animationId -> animation
|
||||
// object map that would require additional memory just to support the use-case of stopping
|
||||
// an animation
|
||||
WritableArray events = null;
|
||||
for (int i = 0; i < mActiveAnimations.size(); i++) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
if (animatedNode.equals(animation.animatedValue)) {
|
||||
if (animation.endCallback != null) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
endCallbackResponse.putDouble("value", animation.animatedValue.nodeValue);
|
||||
animation.endCallback.invoke(endCallbackResponse);
|
||||
} else if (mReactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("animationId", animation.id);
|
||||
params.putBoolean("finished", false);
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
params.putDouble("value", animation.animatedValue.nodeValue);
|
||||
if (events == null) {
|
||||
events = Arguments.createArray();
|
||||
}
|
||||
events.pushMap(params);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if (events != null) {
|
||||
mReactApplicationContext.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void stopAnimation(int animationId) {
|
||||
// in most of the cases there should never be more than a few active animations running at the
|
||||
// same time. Therefore it does not make much sense to create an animationId -> animation
|
||||
// object map that would require additional memory just to support the use-case of stopping
|
||||
// an animation
|
||||
WritableArray events = null;
|
||||
for (int i = 0; i < mActiveAnimations.size(); i++) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
if (animation.id == animationId) {
|
||||
if (animation.endCallback != null) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", false);
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
endCallbackResponse.putDouble("value", animation.animatedValue.nodeValue);
|
||||
animation.endCallback.invoke(endCallbackResponse);
|
||||
} else if (mReactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("animationId", animation.id);
|
||||
params.putBoolean("finished", false);
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
params.putDouble("value", animation.animatedValue.nodeValue);
|
||||
if (events == null) {
|
||||
events = Arguments.createArray();
|
||||
}
|
||||
events.pushMap(params);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (events != null) {
|
||||
mReactApplicationContext.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events);
|
||||
}
|
||||
// Do not throw an error in the case animation could not be found. We only keep "active"
|
||||
// animations in the registry and there is a chance that Animated.js will enqueue a
|
||||
// stopAnimation call after the animation has ended or the call will reach native thread only
|
||||
// when the animation is already over.
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void connectAnimatedNodes(int parentNodeTag, int childNodeTag) {
|
||||
AnimatedNode parentNode = mAnimatedNodes.get(parentNodeTag);
|
||||
if (parentNode == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"connectAnimatedNodes: Animated node with tag (parent) ["
|
||||
+ parentNodeTag
|
||||
+ "] does not exist");
|
||||
}
|
||||
AnimatedNode childNode = mAnimatedNodes.get(childNodeTag);
|
||||
if (childNode == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"connectAnimatedNodes: Animated node with tag (child) ["
|
||||
+ childNodeTag
|
||||
+ "] does not exist");
|
||||
}
|
||||
parentNode.addChild(childNode);
|
||||
mUpdatedNodes.put(childNodeTag, childNode);
|
||||
}
|
||||
|
||||
public void disconnectAnimatedNodes(int parentNodeTag, int childNodeTag) {
|
||||
AnimatedNode parentNode = mAnimatedNodes.get(parentNodeTag);
|
||||
if (parentNode == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"disconnectAnimatedNodes: Animated node with tag (parent) ["
|
||||
+ parentNodeTag
|
||||
+ "] does not exist");
|
||||
}
|
||||
AnimatedNode childNode = mAnimatedNodes.get(childNodeTag);
|
||||
if (childNode == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"disconnectAnimatedNodes: Animated node with tag (child) ["
|
||||
+ childNodeTag
|
||||
+ "] does not exist");
|
||||
}
|
||||
parentNode.removeChild(childNode);
|
||||
mUpdatedNodes.put(childNodeTag, childNode);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void connectAnimatedNodeToView(int animatedNodeTag, int viewTag) {
|
||||
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
|
||||
if (node == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"connectAnimatedNodeToView: Animated node with tag ["
|
||||
+ animatedNodeTag
|
||||
+ "] does not exist");
|
||||
}
|
||||
if (!(node instanceof PropsAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"connectAnimatedNodeToView: Animated node connected to view ["
|
||||
+ viewTag
|
||||
+ "] should be of type "
|
||||
+ PropsAnimatedNode.class.getName());
|
||||
}
|
||||
if (mReactApplicationContext == null) {
|
||||
throw new IllegalStateException(
|
||||
"connectAnimatedNodeToView: Animated node could not be connected, no"
|
||||
+ " ReactApplicationContext: "
|
||||
+ viewTag);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
UIManager uiManager =
|
||||
UIManagerHelper.getUIManagerForReactTag(mReactApplicationContext, viewTag);
|
||||
if (uiManager == null) {
|
||||
ReactSoftExceptionLogger.logSoftException(
|
||||
TAG,
|
||||
new ReactNoCrashSoftException(
|
||||
"connectAnimatedNodeToView: Animated node could not be connected to UIManager -"
|
||||
+ " uiManager disappeared for tag: "
|
||||
+ viewTag));
|
||||
return;
|
||||
}
|
||||
|
||||
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
|
||||
propsAnimatedNode.connectToView(viewTag, uiManager);
|
||||
mUpdatedNodes.put(animatedNodeTag, node);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void disconnectAnimatedNodeFromView(int animatedNodeTag, int viewTag) {
|
||||
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
|
||||
if (node == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"disconnectAnimatedNodeFromView: Animated node with tag ["
|
||||
+ animatedNodeTag
|
||||
+ "] does not exist");
|
||||
}
|
||||
if (!(node instanceof PropsAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"disconnectAnimatedNodeFromView: Animated node connected to view ["
|
||||
+ viewTag
|
||||
+ "] should be of type "
|
||||
+ PropsAnimatedNode.class.getName());
|
||||
}
|
||||
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
|
||||
propsAnimatedNode.disconnectFromView(viewTag);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void getValue(int tag, @Nullable Callback callback) {
|
||||
AnimatedNode node = mAnimatedNodes.get(tag);
|
||||
if (node == null || !(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"getValue: Animated node with tag [" + tag + "] does not exist or is not a 'value' node");
|
||||
}
|
||||
double value = ((ValueAnimatedNode) node).getValue();
|
||||
if (callback != null) {
|
||||
callback.invoke(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no callback, that means that JS is using the single-operation mode, and not
|
||||
// passing any callbacks into Java.
|
||||
// See NativeAnimatedHelper.js for details.
|
||||
// Instead, we use RCTDeviceEventEmitter to pass data back to JS and emulate callbacks.
|
||||
if (mReactApplicationContext == null) {
|
||||
return;
|
||||
}
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("tag", tag);
|
||||
params.putDouble("value", value);
|
||||
mReactApplicationContext.emitDeviceEvent("onNativeAnimatedModuleGetValue", params);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void restoreDefaultValues(int animatedNodeTag) {
|
||||
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
|
||||
// Restoring default values needs to happen before UIManager operations so it is
|
||||
// possible the node hasn't been created yet if it is being connected and
|
||||
// disconnected in the same batch. In that case we don't need to restore
|
||||
// default values since it will never actually update the view.
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
if (!(node instanceof PropsAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"Animated node connected to view [?] should be of type "
|
||||
+ PropsAnimatedNode.class.getName());
|
||||
}
|
||||
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
|
||||
propsAnimatedNode.restoreDefaultValues();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void addAnimatedEventToView(
|
||||
int viewTag, String eventHandlerName, ReadableMap eventMapping) {
|
||||
int nodeTag = eventMapping.getInt("animatedValueTag");
|
||||
AnimatedNode node = mAnimatedNodes.get(nodeTag);
|
||||
if (node == null) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"addAnimatedEventToView: Animated node with tag [" + nodeTag + "] does not exist");
|
||||
}
|
||||
if (!(node instanceof ValueAnimatedNode)) {
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"addAnimatedEventToView: Animated node on view ["
|
||||
+ viewTag
|
||||
+ "] connected to event handler ("
|
||||
+ eventHandlerName
|
||||
+ ") should be of type "
|
||||
+ ValueAnimatedNode.class.getName());
|
||||
}
|
||||
|
||||
ReadableArray path = eventMapping.getArray("nativeEventPath");
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
List<String> pathList = new ArrayList<>(path.size());
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
for (int i = 0; i < path.size(); i++) {
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
pathList.add(path.getString(i));
|
||||
}
|
||||
|
||||
String eventName = normalizeEventName(eventHandlerName);
|
||||
|
||||
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
|
||||
public void removeAnimatedEventFromView(
|
||||
int viewTag, String eventHandlerName, int animatedValueTag) {
|
||||
String eventName = normalizeEventName(eventHandlerName);
|
||||
|
||||
ListIterator<EventAnimationDriver> it = mEventDrivers.listIterator();
|
||||
while (it.hasNext()) {
|
||||
EventAnimationDriver driver = it.next();
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
if (eventName.equals(driver.eventName)
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
&& viewTag == driver.viewTag
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
&& animatedValueTag == driver.valueNode.tag) {
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventName.equals("topScroll")) {
|
||||
// Handle the custom topScrollEnded event sent by the ScrollViews when the user stops dragging
|
||||
removeAnimatedEventFromView(viewTag, "topScrollEnded", animatedValueTag);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEventDispatch(final Event event) {
|
||||
// Events can be dispatched from any thread so we have to make sure handleEvent is run from the
|
||||
// UI thread.
|
||||
if (UiThreadUtil.isOnUiThread()) {
|
||||
handleEvent(event);
|
||||
} else {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handleEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void handleEvent(Event event) {
|
||||
if (mEventDrivers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean foundAtLeastOneDriver = false;
|
||||
Event.EventAnimationDriverMatchSpec matchSpec = event.getEventAnimationDriverMatchSpec();
|
||||
for (EventAnimationDriver driver : mEventDrivers) {
|
||||
if (matchSpec.match(driver.viewTag, driver.eventName)) {
|
||||
foundAtLeastOneDriver = true;
|
||||
stopAnimationsForNode(driver.valueNode);
|
||||
event.dispatchModern(driver);
|
||||
mRunUpdateNodeList.add(driver.valueNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAtLeastOneDriver) {
|
||||
updateNodes(mRunUpdateNodeList);
|
||||
mRunUpdateNodeList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation loop performs two BFSes over the graph of animated nodes. We use incremented {@code
|
||||
* mAnimatedGraphBFSColor} to mark nodes as visited in each of the BFSes which saves additional
|
||||
* loops for clearing "visited" states.
|
||||
*
|
||||
* <p>First BFS starts with nodes that are in {@code mUpdatedNodes} (that is, their value have
|
||||
* been modified from JS in the last batch of JS operations) or directly attached to an active
|
||||
* animation (hence linked to objects from {@code mActiveAnimations}). In that step we calculate
|
||||
* an attribute {@code activeIncomingNodes}. The second BFS runs in topological order over the
|
||||
* sub-graph of *active* nodes. This is done by adding node to the BFS queue only if all its
|
||||
* "predecessors" have already been visited.
|
||||
*/
|
||||
@UiThread
|
||||
public void runUpdates(long frameTimeNanos) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
boolean hasFinishedAnimations = false;
|
||||
|
||||
for (int i = 0; i < mUpdatedNodes.size(); i++) {
|
||||
AnimatedNode node = mUpdatedNodes.valueAt(i);
|
||||
mRunUpdateNodeList.add(node);
|
||||
}
|
||||
|
||||
// Clean mUpdatedNodes queue
|
||||
mUpdatedNodes.clear();
|
||||
|
||||
for (int i = 0; i < mActiveAnimations.size(); i++) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
animation.runAnimationStep(frameTimeNanos);
|
||||
AnimatedNode valueNode = animation.animatedValue;
|
||||
mRunUpdateNodeList.add(valueNode);
|
||||
if (animation.hasFinished) {
|
||||
hasFinishedAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateNodes(mRunUpdateNodeList);
|
||||
mRunUpdateNodeList.clear();
|
||||
|
||||
// Cleanup finished animations. Iterate over the array of animations and override ones that has
|
||||
// finished, then resize `mActiveAnimations`.
|
||||
if (hasFinishedAnimations) {
|
||||
WritableArray events = null;
|
||||
for (int i = mActiveAnimations.size() - 1; i >= 0; i--) {
|
||||
AnimationDriver animation = mActiveAnimations.valueAt(i);
|
||||
if (animation.hasFinished) {
|
||||
if (animation.endCallback != null) {
|
||||
WritableMap endCallbackResponse = Arguments.createMap();
|
||||
endCallbackResponse.putBoolean("finished", true);
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
endCallbackResponse.putDouble("value", animation.animatedValue.nodeValue);
|
||||
animation.endCallback.invoke(endCallbackResponse);
|
||||
} else if (mReactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putInt("animationId", animation.id);
|
||||
params.putBoolean("finished", true);
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
params.putDouble("value", animation.animatedValue.nodeValue);
|
||||
if (events == null) {
|
||||
events = Arguments.createArray();
|
||||
}
|
||||
events.pushMap(params);
|
||||
}
|
||||
mActiveAnimations.removeAt(i);
|
||||
}
|
||||
}
|
||||
if (events != null) {
|
||||
mReactApplicationContext.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.eventName) && tag == driver.viewTag) {
|
||||
tags.add(driver.viewTag);
|
||||
if (driver.valueNode != null && driver.valueNode.children != null) {
|
||||
for (AnimatedNode node : driver.valueNode.children) {
|
||||
tags.add(node.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void updateNodes(List<AnimatedNode> nodes) {
|
||||
int activeNodesCount = 0;
|
||||
int updatedNodesCount = 0;
|
||||
|
||||
// STEP 1.
|
||||
// BFS over graph of nodes. Update `mIncomingNodes` attribute for each node during that BFS.
|
||||
// Store number of visited nodes in `activeNodesCount`. We "execute" active animations as a part
|
||||
// of this step.
|
||||
|
||||
mAnimatedGraphBFSColor++; /* use new color */
|
||||
if (mAnimatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
|
||||
// value "0" is used as an initial color for a new node, using it in BFS may cause some nodes
|
||||
// to be skipped.
|
||||
mAnimatedGraphBFSColor++;
|
||||
}
|
||||
|
||||
Queue<AnimatedNode> nodesQueue = new ArrayDeque<>();
|
||||
for (AnimatedNode node : nodes) {
|
||||
if (node.BFSColor != mAnimatedGraphBFSColor) {
|
||||
node.BFSColor = mAnimatedGraphBFSColor;
|
||||
activeNodesCount++;
|
||||
nodesQueue.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
while (!nodesQueue.isEmpty()) {
|
||||
AnimatedNode nextNode = nodesQueue.poll();
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
if (nextNode.children != null) {
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
for (int i = 0; i < nextNode.children.size(); i++) {
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
AnimatedNode child = nextNode.children.get(i);
|
||||
child.activeIncomingNodes++;
|
||||
if (child.BFSColor != mAnimatedGraphBFSColor) {
|
||||
child.BFSColor = mAnimatedGraphBFSColor;
|
||||
activeNodesCount++;
|
||||
nodesQueue.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 2
|
||||
// BFS over the graph of active nodes in topological order -> visit node only when all its
|
||||
// "predecessors" in the graph have already been visited. It is important to visit nodes in that
|
||||
// order as they may often use values of their predecessors in order to calculate "next state"
|
||||
// of their own. We start by determining the starting set of nodes by looking for nodes with
|
||||
// `activeIncomingNodes = 0` (those can only be the ones that we start BFS in the previous
|
||||
// step). We store number of visited nodes in this step in `updatedNodesCount`
|
||||
|
||||
mAnimatedGraphBFSColor++;
|
||||
if (mAnimatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
|
||||
// see reasoning for this check a few lines above
|
||||
mAnimatedGraphBFSColor++;
|
||||
}
|
||||
|
||||
// find nodes with zero "incoming nodes", those can be either nodes from `mUpdatedNodes` or
|
||||
// ones connected to active animations
|
||||
for (AnimatedNode node : nodes) {
|
||||
if (node.activeIncomingNodes == 0 && node.BFSColor != mAnimatedGraphBFSColor) {
|
||||
node.BFSColor = mAnimatedGraphBFSColor;
|
||||
updatedNodesCount++;
|
||||
nodesQueue.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Run main "update" loop
|
||||
int cyclesDetected = 0;
|
||||
while (!nodesQueue.isEmpty()) {
|
||||
AnimatedNode nextNode = nodesQueue.poll();
|
||||
try {
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
nextNode.update();
|
||||
if (nextNode instanceof PropsAnimatedNode) {
|
||||
// Send property updates to native view manager
|
||||
((PropsAnimatedNode) nextNode).updateView();
|
||||
}
|
||||
} catch (JSApplicationCausedNativeException e) {
|
||||
// An exception is thrown if the view hasn't been created yet. This can happen because
|
||||
// views are created in batches. If this particular view didn't make it into a batch yet,
|
||||
// the view won't exist and an exception will be thrown when attempting to start an
|
||||
// animation on it.
|
||||
//
|
||||
// Eat the exception rather than crashing. The impact is that we may drop one or more
|
||||
// frames of the animation.
|
||||
FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e);
|
||||
}
|
||||
if (nextNode instanceof ValueAnimatedNode) {
|
||||
// Potentially send events to JS when the node's value is updated
|
||||
((ValueAnimatedNode) nextNode).onValueUpdate();
|
||||
}
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
if (nextNode.children != null) {
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
for (int i = 0; i < nextNode.children.size(); i++) {
|
||||
// NULLSAFE_FIXME[Nullable Dereference]
|
||||
AnimatedNode child = nextNode.children.get(i);
|
||||
child.activeIncomingNodes--;
|
||||
if (child.BFSColor != mAnimatedGraphBFSColor && child.activeIncomingNodes == 0) {
|
||||
child.BFSColor = mAnimatedGraphBFSColor;
|
||||
updatedNodesCount++;
|
||||
nodesQueue.add(child);
|
||||
} else if (child.BFSColor == mAnimatedGraphBFSColor) {
|
||||
cyclesDetected++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that we've visited *all* active nodes. Throw otherwise as this could mean there is a
|
||||
// cycle in animated node graph, or that the graph is only partially set up. We also take
|
||||
// advantage of the fact that all active nodes are visited in the step above so that all the
|
||||
// nodes properties `activeIncomingNodes` are set to zero.
|
||||
// In Fabric there can be race conditions between the JS thread setting up or tearing down
|
||||
// animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent
|
||||
// states.
|
||||
if (activeNodesCount != updatedNodesCount) {
|
||||
if (mWarnedAboutGraphTraversal) {
|
||||
return;
|
||||
}
|
||||
mWarnedAboutGraphTraversal = true;
|
||||
|
||||
// Before crashing or logging soft exception, log details about current graph setup
|
||||
FLog.e(TAG, "Detected animation cycle or disconnected graph. ");
|
||||
for (AnimatedNode node : nodes) {
|
||||
FLog.e(TAG, node.prettyPrintWithChildren());
|
||||
}
|
||||
|
||||
// If we're running only in non-Fabric, we still throw an exception.
|
||||
// In Fabric, it seems that animations enter an inconsistent state fairly often.
|
||||
// We detect if the inconsistency is due to a cycle (a fatal error for which we must crash)
|
||||
// or disconnected regions, indicating a partially-set-up animation graph, which is not
|
||||
// fatal and can stay a warning.
|
||||
String reason =
|
||||
cyclesDetected > 0 ? "cycles (" + cyclesDetected + ")" : "disconnected regions";
|
||||
IllegalStateException ex =
|
||||
new IllegalStateException(
|
||||
"Looks like animated nodes graph has "
|
||||
+ reason
|
||||
+ ", there are "
|
||||
+ activeNodesCount
|
||||
+ " but toposort visited only "
|
||||
+ updatedNodesCount);
|
||||
if (mEventListenerInitializedForFabric && cyclesDetected == 0) {
|
||||
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
|
||||
// or fix the root cause
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, new ReactNoCrashSoftException(ex));
|
||||
} else if (mEventListenerInitializedForFabric) {
|
||||
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
|
||||
// or fix the root cause
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, new ReactNoCrashSoftException(ex));
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
} else {
|
||||
mWarnedAboutGraphTraversal = false;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeEventName(String eventHandlerName) {
|
||||
// Fabric UIManager also makes this assumption
|
||||
String eventName = eventHandlerName;
|
||||
if (eventHandlerName.startsWith("on")) {
|
||||
eventName = "top" + eventHandlerName.substring(2);
|
||||
}
|
||||
|
||||
return eventName;
|
||||
}
|
||||
}
|
||||
+768
@@ -0,0 +1,768 @@
|
||||
/*
|
||||
* 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.animated
|
||||
|
||||
import android.util.SparseArray
|
||||
import androidx.annotation.UiThread
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Callback
|
||||
import com.facebook.react.bridge.JSApplicationCausedNativeException
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactNoCrashSoftException
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.UiThreadUtil
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import com.facebook.react.uimanager.common.UIManagerType
|
||||
import com.facebook.react.uimanager.events.Event
|
||||
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.Queue
|
||||
|
||||
/**
|
||||
* This is the main class that coordinates how native animated JS implementation drives UI changes.
|
||||
*
|
||||
* It implements a management interface for animated nodes graph as well as implements a graph
|
||||
* traversal algorithm that is run for each animation frame.
|
||||
*
|
||||
* For each animation frame we visit animated nodes that might've been updated as well as their
|
||||
* children that may use parent's values to update themselves. At the end of the traversal algorithm
|
||||
* we expect to reach a special type of the node: PropsAnimatedNode that is then responsible for
|
||||
* calculating property map which can be sent to native view hierarchy to update the view.
|
||||
*
|
||||
* IMPORTANT: This class should be accessed only from the UI Thread
|
||||
*/
|
||||
public class NativeAnimatedNodesManager(
|
||||
private val reactApplicationContext: ReactApplicationContext?
|
||||
) : EventDispatcherListener {
|
||||
|
||||
private val animatedNodes = SparseArray<AnimatedNode>()
|
||||
private val activeAnimations = SparseArray<AnimationDriver>()
|
||||
private val updatedNodes = SparseArray<AnimatedNode>()
|
||||
|
||||
// List of event animation drivers for an event on view.
|
||||
// There may be multiple drivers for the same event and view.
|
||||
private val eventDrivers: MutableList<EventAnimationDriver> = ArrayList()
|
||||
private var animatedGraphBFSColor = 0
|
||||
|
||||
// Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`.
|
||||
private val runUpdateNodeList: MutableList<AnimatedNode> = LinkedList()
|
||||
|
||||
private var eventListenerInitializedForFabric = false
|
||||
private var eventListenerInitializedForNonFabric = false
|
||||
|
||||
private var warnedAboutGraphTraversal = false
|
||||
|
||||
/**
|
||||
* Initialize event listeners for Fabric UIManager or non-Fabric UIManager, exactly once. Once
|
||||
* Fabric is the only UIManager, this logic can be simplified. This is expected to only be called
|
||||
* from the native module thread.
|
||||
*
|
||||
* @param uiManagerType
|
||||
*/
|
||||
public fun initializeEventListenerForUIManagerType(@UIManagerType uiManagerType: Int): Unit {
|
||||
val isEventListenerInitialized =
|
||||
when (uiManagerType) {
|
||||
UIManagerType.FABRIC -> eventListenerInitializedForFabric
|
||||
else -> eventListenerInitializedForNonFabric
|
||||
}
|
||||
|
||||
if (isEventListenerInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
val uiManager =
|
||||
UIManagerHelper.getUIManager(checkNotNull(reactApplicationContext), uiManagerType)
|
||||
if (uiManager != null) {
|
||||
uiManager.eventDispatcher.addListener(this)
|
||||
if (uiManagerType == UIManagerType.FABRIC) {
|
||||
eventListenerInitializedForFabric = true
|
||||
} else {
|
||||
eventListenerInitializedForNonFabric = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun getNodeById(id: Int): AnimatedNode? = animatedNodes.get(id)
|
||||
|
||||
public fun hasActiveAnimations(): Boolean = activeAnimations.size() > 0 || updatedNodes.size() > 0
|
||||
|
||||
@UiThread
|
||||
public fun createAnimatedNode(tag: Int, config: ReadableMap): Unit {
|
||||
if (animatedNodes.get(tag) != null) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
"createAnimatedNode: Animated node [$tag] already exists")
|
||||
}
|
||||
val type = config.getString("type")
|
||||
val node =
|
||||
when (type) {
|
||||
"style" -> StyleAnimatedNode(config, this)
|
||||
"value" -> ValueAnimatedNode(config)
|
||||
"color" -> ColorAnimatedNode(config, this, checkNotNull(reactApplicationContext))
|
||||
"props" -> PropsAnimatedNode(config, this)
|
||||
"interpolation" -> InterpolationAnimatedNode(config)
|
||||
"addition" -> AdditionAnimatedNode(config, this)
|
||||
"subtraction" -> SubtractionAnimatedNode(config, this)
|
||||
"division" -> DivisionAnimatedNode(config, this)
|
||||
"multiplication" -> MultiplicationAnimatedNode(config, this)
|
||||
"modulus" -> ModulusAnimatedNode(config, this)
|
||||
"diffclamp" -> DiffClampAnimatedNode(config, this)
|
||||
"transform" -> TransformAnimatedNode(config, this)
|
||||
"tracking" -> TrackingAnimatedNode(config, this)
|
||||
"object" -> ObjectAnimatedNode(config, this)
|
||||
else -> throw JSApplicationIllegalArgumentException("Unsupported node type: $type")
|
||||
}
|
||||
node.tag = tag
|
||||
animatedNodes.put(tag, node)
|
||||
updatedNodes.put(tag, node)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun updateAnimatedNodeConfig(tag: Int, config: ReadableMap?): Unit {
|
||||
val node =
|
||||
animatedNodes.get(tag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
"updateAnimatedNode: Animated node [$tag] does not exist")
|
||||
|
||||
if (node is AnimatedNodeWithUpdateableConfig) {
|
||||
stopAnimationsForNode(node)
|
||||
(node as AnimatedNodeWithUpdateableConfig).onUpdateConfig(config)
|
||||
updatedNodes.put(tag, node)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun dropAnimatedNode(tag: Int): Unit {
|
||||
animatedNodes.remove(tag)
|
||||
updatedNodes.remove(tag)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun startListeningToAnimatedNodeValue(
|
||||
tag: Int,
|
||||
listener: AnimatedNodeValueListener?
|
||||
): Unit {
|
||||
val node = animatedNodes[tag]
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("startListeningToAnimatedNodeValue: Animated node [${tag}] does not exist, or is not a 'value' node"))
|
||||
}
|
||||
node.setValueListener(listener)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun stopListeningToAnimatedNodeValue(tag: Int): Unit {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("startListeningToAnimatedNodeValue: Animated node [${tag}] does not exist, or is not a 'value' node"))
|
||||
}
|
||||
node.setValueListener(null)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun setAnimatedNodeValue(tag: Int, value: Double): Unit {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("setAnimatedNodeValue: Animated node [${tag}] does not exist, or is not a 'value' node"))
|
||||
}
|
||||
stopAnimationsForNode(node)
|
||||
node.nodeValue = value
|
||||
updatedNodes.put(tag, node)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun setAnimatedNodeOffset(tag: Int, offset: Double): Unit {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("setAnimatedNodeOffset: Animated node [${tag}] does not exist, or is not a 'value' node"))
|
||||
}
|
||||
node.offset = offset
|
||||
updatedNodes.put(tag, node)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun flattenAnimatedNodeOffset(tag: Int): Unit {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("flattenAnimatedNodeOffset: Animated node [${tag}] does not exist, or is not a 'value' node"))
|
||||
}
|
||||
node.flattenOffset()
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun extractAnimatedNodeOffset(tag: Int): Unit {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("extractAnimatedNodeOffset: Animated node [${tag}] does not exist, or is not a 'value' node"))
|
||||
}
|
||||
node.extractOffset()
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun startAnimatingNode(
|
||||
animationId: Int,
|
||||
animatedNodeTag: Int,
|
||||
animationConfig: ReadableMap,
|
||||
endCallback: Callback?
|
||||
): Unit {
|
||||
val node =
|
||||
animatedNodes.get(animatedNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
"startAnimatingNode: Animated node [$animatedNodeTag] does not exist")
|
||||
if (node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("startAnimatingNode: Animated node [${animatedNodeTag}] should be of type ${ValueAnimatedNode::class.java.name}"))
|
||||
}
|
||||
|
||||
val existingDriver = activeAnimations[animationId]
|
||||
if (existingDriver != null) {
|
||||
// animation with the given ID is already running, we need to update its configuration instead
|
||||
// of spawning a new one
|
||||
existingDriver.resetConfig(animationConfig)
|
||||
return
|
||||
}
|
||||
|
||||
val type = animationConfig.getString("type")
|
||||
val animation =
|
||||
when (type) {
|
||||
"frames" -> FrameBasedAnimationDriver(animationConfig)
|
||||
"spring" -> SpringAnimation(animationConfig)
|
||||
"decay" -> DecayAnimation(animationConfig)
|
||||
else -> {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
"startAnimatingNode: Unsupported animation type [$animatedNodeTag]: $type")
|
||||
}
|
||||
}
|
||||
animation.id = animationId
|
||||
animation.endCallback = endCallback
|
||||
animation.animatedValue = node
|
||||
activeAnimations.put(animationId, animation)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun stopAnimationsForNode(animatedNode: AnimatedNode) {
|
||||
// in most of the cases there should never be more than a few active animations running at the
|
||||
// same time. Therefore it does not make much sense to create an animationId -> animation
|
||||
// object map that would require additional memory just to support the use-case of stopping
|
||||
// an animation
|
||||
var events: WritableArray? = null
|
||||
var i = 0
|
||||
while (i < activeAnimations.size()) {
|
||||
val animation = activeAnimations.valueAt(i)
|
||||
if (animatedNode == animation.animatedValue) {
|
||||
val animatedValueNonnull = checkNotNull(animation.animatedValue)
|
||||
if (animation.endCallback != null) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
val endCallbackResponse = Arguments.createMap()
|
||||
endCallbackResponse.putBoolean("finished", false)
|
||||
endCallbackResponse.putDouble("value", animatedValueNonnull.nodeValue)
|
||||
animation.endCallback?.invoke(endCallbackResponse)
|
||||
} else if (reactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
val params = Arguments.createMap()
|
||||
params.putInt("animationId", animation.id)
|
||||
params.putBoolean("finished", false)
|
||||
params.putDouble("value", animatedValueNonnull.nodeValue)
|
||||
events = events ?: Arguments.createArray()
|
||||
events.pushMap(params)
|
||||
}
|
||||
activeAnimations.removeAt(i)
|
||||
i--
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (events != null) {
|
||||
reactApplicationContext?.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun stopAnimation(animationId: Int): Unit {
|
||||
// in most of the cases there should never be more than a few active animations running at the
|
||||
// same time. Therefore it does not make much sense to create an animationId -> animation
|
||||
// object map that would require additional memory just to support the use-case of stopping
|
||||
// an animation
|
||||
var events: WritableArray? = null
|
||||
for (i in 0..<activeAnimations.size()) {
|
||||
val animation = activeAnimations.valueAt(i)
|
||||
if (animation.id == animationId) {
|
||||
if (animation.endCallback != null) {
|
||||
// Invoke animation end callback with {finished: false}
|
||||
val endCallbackResponse = Arguments.createMap()
|
||||
endCallbackResponse.putBoolean("finished", false)
|
||||
endCallbackResponse.putDouble("value", checkNotNull(animation.animatedValue).nodeValue)
|
||||
checkNotNull(animation.endCallback).invoke(endCallbackResponse)
|
||||
} else if (reactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
val params = Arguments.createMap()
|
||||
params.putInt("animationId", animation.id)
|
||||
params.putBoolean("finished", false)
|
||||
params.putDouble("value", checkNotNull(animation.animatedValue).nodeValue)
|
||||
events = events ?: Arguments.createArray()
|
||||
events.pushMap(params)
|
||||
}
|
||||
activeAnimations.removeAt(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (events != null) {
|
||||
reactApplicationContext?.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events)
|
||||
}
|
||||
// Do not throw an error in the case animation could not be found. We only keep "active"
|
||||
// animations in the registry and there is a chance that Animated.js will enqueue a
|
||||
// stopAnimation call after the animation has ended or the call will reach native thread only
|
||||
// when the animation is already over.
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun connectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int): Unit {
|
||||
val parentNode =
|
||||
animatedNodes.get(parentNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
("connectAnimatedNodes: Animated node with tag (parent) [${parentNodeTag}] does not exist"))
|
||||
val childNode =
|
||||
animatedNodes.get(childNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
("connectAnimatedNodes: Animated node with tag (child) [${childNodeTag}] does not exist"))
|
||||
parentNode.addChild(childNode)
|
||||
updatedNodes.put(childNodeTag, childNode)
|
||||
}
|
||||
|
||||
public fun disconnectAnimatedNodes(parentNodeTag: Int, childNodeTag: Int): Unit {
|
||||
val parentNode =
|
||||
animatedNodes.get(parentNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
("disconnectAnimatedNodes: Animated node with tag (parent) [${parentNodeTag}] does not exist"))
|
||||
val childNode =
|
||||
animatedNodes.get(childNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
("disconnectAnimatedNodes: Animated node with tag (child) [${childNodeTag}] does not exist"))
|
||||
parentNode.removeChild(childNode)
|
||||
updatedNodes.put(childNodeTag, childNode)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun connectAnimatedNodeToView(animatedNodeTag: Int, viewTag: Int): Unit {
|
||||
val node =
|
||||
animatedNodes.get(animatedNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
("connectAnimatedNodeToView: Animated node with tag [${animatedNodeTag}] does not exist"))
|
||||
if (node !is PropsAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("connectAnimatedNodeToView: Animated node connected to view [${viewTag}] should be of type ${PropsAnimatedNode::class.java.name}"))
|
||||
}
|
||||
checkNotNull(reactApplicationContext) {
|
||||
("connectAnimatedNodeToView: Animated node could not be connected, no ReactApplicationContext: ${viewTag}")
|
||||
}
|
||||
|
||||
val uiManager = UIManagerHelper.getUIManagerForReactTag(reactApplicationContext, viewTag)
|
||||
if (uiManager == null) {
|
||||
ReactSoftExceptionLogger.logSoftException(
|
||||
TAG,
|
||||
ReactNoCrashSoftException(
|
||||
("connectAnimatedNodeToView: Animated node could not be connected to UIManager - uiManager disappeared for tag: ${viewTag}")))
|
||||
return
|
||||
}
|
||||
|
||||
node.connectToView(viewTag, uiManager)
|
||||
updatedNodes.put(animatedNodeTag, node)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun disconnectAnimatedNodeFromView(animatedNodeTag: Int, viewTag: Int): Unit {
|
||||
val node =
|
||||
animatedNodes.get(animatedNodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
("disconnectAnimatedNodeFromView: Animated node with tag [${animatedNodeTag}] does not exist"))
|
||||
if (node !is PropsAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("disconnectAnimatedNodeFromView: Animated node connected to view [${viewTag}] should be of type ${PropsAnimatedNode::class.java.name}"))
|
||||
}
|
||||
node.disconnectFromView(viewTag)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun getValue(tag: Int, callback: Callback?): Unit {
|
||||
val node = animatedNodes.get(tag)
|
||||
if (node == null || node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
"getValue: Animated node with tag [$tag] does not exist or is not a 'value' node")
|
||||
}
|
||||
val value = node.getValue()
|
||||
if (callback != null) {
|
||||
callback.invoke(value)
|
||||
return
|
||||
}
|
||||
|
||||
// If there's no callback, that means that JS is using the single-operation mode, and not
|
||||
// passing any callbacks into Java.
|
||||
// See NativeAnimatedHelper.js for details.
|
||||
// Instead, we use RCTDeviceEventEmitter to pass data back to JS and emulate callbacks.
|
||||
if (reactApplicationContext == null) {
|
||||
return
|
||||
}
|
||||
val params = Arguments.createMap()
|
||||
params.putInt("tag", tag)
|
||||
params.putDouble("value", value)
|
||||
reactApplicationContext.emitDeviceEvent("onNativeAnimatedModuleGetValue", params)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun restoreDefaultValues(animatedNodeTag: Int): Unit {
|
||||
val node = animatedNodes.get(animatedNodeTag) ?: return
|
||||
// Restoring default values needs to happen before UIManager operations so it is
|
||||
// possible the node hasn't been created yet if it is being connected and
|
||||
// disconnected in the same batch. In that case we don't need to restore
|
||||
// default values since it will never actually update the view.
|
||||
if (node !is PropsAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
"Animated node connected to view [?] should be of type ${PropsAnimatedNode::class.java.name}")
|
||||
}
|
||||
node.restoreDefaultValues()
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun addAnimatedEventToView(
|
||||
viewTag: Int,
|
||||
eventHandlerName: String,
|
||||
eventMapping: ReadableMap
|
||||
): Unit {
|
||||
val nodeTag = eventMapping.getInt("animatedValueTag")
|
||||
val node =
|
||||
animatedNodes.get(nodeTag)
|
||||
?: throw JSApplicationIllegalArgumentException(
|
||||
"addAnimatedEventToView: Animated node with tag [$nodeTag] does not exist")
|
||||
if (node !is ValueAnimatedNode) {
|
||||
throw JSApplicationIllegalArgumentException(
|
||||
("addAnimatedEventToView: Animated node on view [${viewTag}] connected to event handler (${eventHandlerName}) should be of type ${ValueAnimatedNode::class.java.name}"))
|
||||
}
|
||||
|
||||
val path = checkNotNull(eventMapping.getArray("nativeEventPath"))
|
||||
val pathList: MutableList<String> = ArrayList(path.size())
|
||||
for (i in 0..<path.size()) {
|
||||
pathList.add(checkNotNull(path.getString(i)))
|
||||
}
|
||||
|
||||
val eventName = normalizeEventName(eventHandlerName)
|
||||
|
||||
val eventDriver = EventAnimationDriver(eventName, viewTag, pathList, node)
|
||||
eventDrivers.add(eventDriver)
|
||||
|
||||
if (eventName == "topScroll") {
|
||||
// Handle the custom topScrollEnded event sent by the ScrollViews when the user stops dragging
|
||||
addAnimatedEventToView(viewTag, "topScrollEnded", eventMapping)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun removeAnimatedEventFromView(
|
||||
viewTag: Int,
|
||||
eventHandlerName: String,
|
||||
animatedValueTag: Int
|
||||
): Unit {
|
||||
val eventName = normalizeEventName(eventHandlerName)
|
||||
|
||||
eventDrivers
|
||||
.find { driver ->
|
||||
eventName == driver.eventName &&
|
||||
viewTag == driver.viewTag &&
|
||||
animatedValueTag == driver.valueNode.tag
|
||||
}
|
||||
?.let { driver -> eventDrivers.remove(driver) }
|
||||
|
||||
if (eventName == "topScroll") {
|
||||
// Handle the custom topScrollEnded event sent by the ScrollViews when the user stops dragging
|
||||
removeAnimatedEventFromView(viewTag, "topScrollEnded", animatedValueTag)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventDispatch(event: Event<*>) {
|
||||
// Events can be dispatched from any thread so we have to make sure handleEvent is run from the
|
||||
// UI thread.
|
||||
if (UiThreadUtil.isOnUiThread()) {
|
||||
handleEvent(event)
|
||||
} else {
|
||||
UiThreadUtil.runOnUiThread { handleEvent(event) }
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun handleEvent(event: Event<*>) {
|
||||
if (eventDrivers.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
var foundAtLeastOneDriver = false
|
||||
val matchSpec = event.eventAnimationDriverMatchSpec
|
||||
for (driver in eventDrivers) {
|
||||
if (matchSpec.match(driver.viewTag, driver.eventName)) {
|
||||
foundAtLeastOneDriver = true
|
||||
stopAnimationsForNode(driver.valueNode)
|
||||
event.dispatchModern(driver)
|
||||
runUpdateNodeList.add(driver.valueNode)
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAtLeastOneDriver) {
|
||||
updateNodes(runUpdateNodeList)
|
||||
runUpdateNodeList.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation loop performs two BFSes over the graph of animated nodes. We use incremented
|
||||
* `mAnimatedGraphBFSColor` to mark nodes as visited in each of the BFSes which saves additional
|
||||
* loops for clearing "visited" states.
|
||||
*
|
||||
* First BFS starts with nodes that are in `mUpdatedNodes` (that is, their value have been
|
||||
* modified from JS in the last batch of JS operations) or directly attached to an active
|
||||
* animation (hence linked to objects from `mActiveAnimations`). In that step we calculate an
|
||||
* attribute `activeIncomingNodes`. The second BFS runs in topological order over the sub-graph of
|
||||
* *active* nodes. This is done by adding node to the BFS queue only if all its "predecessors"
|
||||
* have already been visited.
|
||||
*/
|
||||
@UiThread
|
||||
public fun runUpdates(frameTimeNanos: Long): Unit {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
var hasFinishedAnimations = false
|
||||
|
||||
for (i in 0..<updatedNodes.size()) {
|
||||
val node = updatedNodes.valueAt(i)
|
||||
runUpdateNodeList.add(node)
|
||||
}
|
||||
|
||||
// Clean mUpdatedNodes queue
|
||||
updatedNodes.clear()
|
||||
|
||||
for (i in 0..<activeAnimations.size()) {
|
||||
val animation = activeAnimations.valueAt(i)
|
||||
animation.runAnimationStep(frameTimeNanos)
|
||||
animation.animatedValue?.let { valueNode -> runUpdateNodeList.add(valueNode) }
|
||||
if (animation.hasFinished) {
|
||||
hasFinishedAnimations = true
|
||||
}
|
||||
}
|
||||
|
||||
updateNodes(runUpdateNodeList)
|
||||
runUpdateNodeList.clear()
|
||||
|
||||
// Cleanup finished animations. Iterate over the array of animations and override ones that has
|
||||
// finished, then resize `mActiveAnimations`.
|
||||
if (hasFinishedAnimations) {
|
||||
var events: WritableArray? = null
|
||||
for (i in activeAnimations.size() - 1 downTo 0) {
|
||||
val animation = activeAnimations.valueAt(i)
|
||||
if (animation.hasFinished) {
|
||||
val animatedValueNonnull = checkNotNull(animation.animatedValue)
|
||||
if (animation.endCallback != null) {
|
||||
val endCallbackResponse = Arguments.createMap()
|
||||
endCallbackResponse.putBoolean("finished", true)
|
||||
endCallbackResponse.putDouble("value", animatedValueNonnull.nodeValue)
|
||||
animation.endCallback?.invoke(endCallbackResponse)
|
||||
} else if (reactApplicationContext != null) {
|
||||
// If no callback is passed in, this /may/ be an animation set up by the single-op
|
||||
// instruction from JS, meaning that no jsi::functions are passed into native and
|
||||
// we communicate via RCTDeviceEventEmitter instead of callbacks.
|
||||
val params = Arguments.createMap()
|
||||
params.putInt("animationId", animation.id)
|
||||
params.putBoolean("finished", true)
|
||||
params.putDouble("value", animatedValueNonnull.nodeValue)
|
||||
events = events ?: Arguments.createArray()
|
||||
events.pushMap(params)
|
||||
}
|
||||
activeAnimations.removeAt(i)
|
||||
}
|
||||
}
|
||||
if (events != null) {
|
||||
reactApplicationContext?.emitDeviceEvent("onNativeAnimatedModuleAnimationFinished", events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getTagsOfConnectedNodes(tag: Int, eventName: String): Set<Int> {
|
||||
val tags: MutableSet<Int> = HashSet()
|
||||
|
||||
// Filter only relevant animation drivers
|
||||
|
||||
eventDrivers.forEach { driver ->
|
||||
if (eventName == driver.eventName && tag == driver.viewTag) {
|
||||
tags.add(driver.viewTag)
|
||||
driver.valueNode.children?.forEach { node -> tags.add(node.tag) }
|
||||
}
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun updateNodes(nodes: List<AnimatedNode>) {
|
||||
var activeNodesCount = 0
|
||||
var updatedNodesCount = 0
|
||||
|
||||
// STEP 1.
|
||||
// BFS over graph of nodes. Update `mIncomingNodes` attribute for each node during that BFS.
|
||||
// Store number of visited nodes in `activeNodesCount`. We "execute" active animations as a part
|
||||
// of this step.
|
||||
animatedGraphBFSColor++ /* use new color */
|
||||
if (animatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
|
||||
// value "0" is used as an initial color for a new node, using it in BFS may cause some nodes
|
||||
// to be skipped.
|
||||
animatedGraphBFSColor++
|
||||
}
|
||||
|
||||
val nodesQueue: Queue<AnimatedNode> = ArrayDeque()
|
||||
for (node in nodes) {
|
||||
if (node.BFSColor != animatedGraphBFSColor) {
|
||||
node.BFSColor = animatedGraphBFSColor
|
||||
activeNodesCount++
|
||||
nodesQueue.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
while (nodesQueue.isNotEmpty()) {
|
||||
val nextNode = nodesQueue.poll()
|
||||
nextNode?.children?.forEach { child ->
|
||||
child.activeIncomingNodes++
|
||||
if (child.BFSColor != animatedGraphBFSColor) {
|
||||
child.BFSColor = animatedGraphBFSColor
|
||||
activeNodesCount++
|
||||
nodesQueue.add(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 2
|
||||
// BFS over the graph of active nodes in topological order -> visit node only when all its
|
||||
// "predecessors" in the graph have already been visited. It is important to visit nodes in that
|
||||
// order as they may often use values of their predecessors in order to calculate "next state"
|
||||
// of their own. We start by determining the starting set of nodes by looking for nodes with
|
||||
// `activeIncomingNodes = 0` (those can only be the ones that we start BFS in the previous
|
||||
// step). We store number of visited nodes in this step in `updatedNodesCount`
|
||||
animatedGraphBFSColor++
|
||||
if (animatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
|
||||
// see reasoning for this check a few lines above
|
||||
animatedGraphBFSColor++
|
||||
}
|
||||
|
||||
// find nodes with zero "incoming nodes", those can be either nodes from `mUpdatedNodes` or
|
||||
// ones connected to active animations
|
||||
for (node in nodes) {
|
||||
if (node.activeIncomingNodes == 0 && node.BFSColor != animatedGraphBFSColor) {
|
||||
node.BFSColor = animatedGraphBFSColor
|
||||
updatedNodesCount++
|
||||
nodesQueue.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
// Run main "update" loop
|
||||
var cyclesDetected = 0
|
||||
while (!nodesQueue.isEmpty()) {
|
||||
val nextNode = nodesQueue.poll()
|
||||
try {
|
||||
nextNode?.update()
|
||||
if (nextNode is PropsAnimatedNode) {
|
||||
// Send property updates to native view manager
|
||||
nextNode.updateView()
|
||||
}
|
||||
} catch (e: JSApplicationCausedNativeException) {
|
||||
// An exception is thrown if the view hasn't been created yet. This can happen because
|
||||
// views are created in batches. If this particular view didn't make it into a batch yet,
|
||||
// the view won't exist and an exception will be thrown when attempting to start an
|
||||
// animation on it.
|
||||
//
|
||||
// Eat the exception rather than crashing. The impact is that we may drop one or more
|
||||
// frames of the animation.
|
||||
FLog.e(TAG, "Native animation workaround, frame lost as result of race condition", e)
|
||||
}
|
||||
if (nextNode is ValueAnimatedNode) {
|
||||
// Potentially send events to JS when the node's value is updated
|
||||
nextNode.onValueUpdate()
|
||||
}
|
||||
nextNode?.children?.forEach { child ->
|
||||
child.activeIncomingNodes--
|
||||
if (child.BFSColor != animatedGraphBFSColor && child.activeIncomingNodes == 0) {
|
||||
child.BFSColor = animatedGraphBFSColor
|
||||
updatedNodesCount++
|
||||
nodesQueue.add(child)
|
||||
} else if (child.BFSColor == animatedGraphBFSColor) {
|
||||
cyclesDetected++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that we've visited *all* active nodes. Throw otherwise as this could mean there is a
|
||||
// cycle in animated node graph, or that the graph is only partially set up. We also take
|
||||
// advantage of the fact that all active nodes are visited in the step above so that all the
|
||||
// nodes properties `activeIncomingNodes` are set to zero.
|
||||
// In Fabric there can be race conditions between the JS thread setting up or tearing down
|
||||
// animated nodes, and Fabric executing them on the UI thread, leading to temporary inconsistent
|
||||
// states.
|
||||
if (activeNodesCount != updatedNodesCount) {
|
||||
if (warnedAboutGraphTraversal) {
|
||||
return
|
||||
}
|
||||
warnedAboutGraphTraversal = true
|
||||
|
||||
// Before crashing or logging soft exception, log details about current graph setup
|
||||
FLog.e(TAG, "Detected animation cycle or disconnected graph. ")
|
||||
for (node in nodes) {
|
||||
FLog.e(TAG, node.prettyPrintWithChildren())
|
||||
}
|
||||
|
||||
// If we're running only in non-Fabric, we still throw an exception.
|
||||
// In Fabric, it seems that animations enter an inconsistent state fairly often.
|
||||
// We detect if the inconsistency is due to a cycle (a fatal error for which we must crash)
|
||||
// or disconnected regions, indicating a partially-set-up animation graph, which is not
|
||||
// fatal and can stay a warning.
|
||||
val reason = if (cyclesDetected > 0) ("cycles ($cyclesDetected)") else "disconnected regions"
|
||||
val ex =
|
||||
IllegalStateException(
|
||||
("Looks like animated nodes graph has ${reason}, there are ${activeNodesCount} but toposort visited only ${updatedNodesCount}"))
|
||||
if (eventListenerInitializedForFabric && cyclesDetected == 0) {
|
||||
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
|
||||
// or fix the root cause
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, ReactNoCrashSoftException(ex))
|
||||
} else if (eventListenerInitializedForFabric) {
|
||||
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
|
||||
// or fix the root cause
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, ReactNoCrashSoftException(ex))
|
||||
} else {
|
||||
throw ex
|
||||
}
|
||||
} else {
|
||||
warnedAboutGraphTraversal = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeEventName(eventHandlerName: String): String =
|
||||
// Fabric UIManager also makes this assumption
|
||||
if (eventHandlerName.startsWith("on")) {
|
||||
"top${eventHandlerName.substring(2)}"
|
||||
} else {
|
||||
eventHandlerName
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "NativeAnimatedNodesManager"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user