Skip to content

Commit ac43548

Browse files
janicduplessisfacebook-github-bot
authored andcommitted
Native Animated - Restore default values when removing props on Android
Summary: Same as #11819 but for Android. I didn't notice the bug initially in my app because I was using different animations on Android which did not trigger this issue. **Test plan** Created a simple repro example and tested that this fixes it. https://gist.github.com/janicduplessis/0f3eb362dae63fedf99a0d3ee041796a Closes #12842 Differential Revision: D5556439 Pulled By: hramos fbshipit-source-id: d13f4ad258d03cca46c793751ebc49d942b99152
1 parent b4f91be commit ac43548

10 files changed

+351
-71
lines changed

ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java

+57-41
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
import com.facebook.react.bridge.Arguments;
1818
import com.facebook.react.bridge.Callback;
1919
import com.facebook.react.bridge.LifecycleEventListener;
20-
import com.facebook.react.bridge.OnBatchCompleteListener;
2120
import com.facebook.react.bridge.ReactApplicationContext;
2221
import com.facebook.react.bridge.ReactContextBaseJavaModule;
2322
import com.facebook.react.bridge.ReactMethod;
2423
import com.facebook.react.bridge.ReadableMap;
2524
import com.facebook.react.bridge.WritableMap;
25+
import com.facebook.react.common.annotations.VisibleForTesting;
2626
import com.facebook.react.module.annotations.ReactModule;
2727
import com.facebook.react.modules.core.DeviceEventManagerModule;
2828
import com.facebook.react.modules.core.ReactChoreographer;
2929
import com.facebook.react.uimanager.GuardedFrameCallback;
30+
import com.facebook.react.uimanager.NativeViewHierarchyManager;
31+
import com.facebook.react.uimanager.UIBlock;
3032
import com.facebook.react.uimanager.UIManagerModule;
33+
import com.facebook.react.uimanager.UIManagerModuleListener;
3134

3235
/**
3336
* Module that exposes interface for creating and managing animated nodes on the "native" side.
@@ -72,19 +75,18 @@
7275
*/
7376
@ReactModule(name = NativeAnimatedModule.NAME)
7477
public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
75-
OnBatchCompleteListener, LifecycleEventListener {
78+
LifecycleEventListener, UIManagerModuleListener {
7679

7780
protected static final String NAME = "NativeAnimatedModule";
7881

7982
private interface UIThreadOperation {
8083
void execute(NativeAnimatedNodesManager animatedNodesManager);
8184
}
8285

83-
private final Object mOperationsCopyLock = new Object();
8486
private final GuardedFrameCallback mAnimatedFrameCallback;
8587
private final ReactChoreographer mReactChoreographer;
8688
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
87-
private volatile @Nullable ArrayList<UIThreadOperation> mReadyOperations = null;
89+
private ArrayList<UIThreadOperation> mPreOperations = new ArrayList<>();
8890

8991
private @Nullable NativeAnimatedNodesManager mNodesManager;
9092

@@ -95,26 +97,9 @@ public NativeAnimatedModule(ReactApplicationContext reactContext) {
9597
mAnimatedFrameCallback = new GuardedFrameCallback(reactContext) {
9698
@Override
9799
protected void doFrameGuarded(final long frameTimeNanos) {
98-
if (mNodesManager == null) {
99-
UIManagerModule uiManager = getReactApplicationContext()
100-
.getNativeModule(UIManagerModule.class);
101-
mNodesManager = new NativeAnimatedNodesManager(uiManager);
102-
}
103-
104-
ArrayList<UIThreadOperation> operations;
105-
synchronized (mOperationsCopyLock) {
106-
operations = mReadyOperations;
107-
mReadyOperations = null;
108-
}
109-
110-
if (operations != null) {
111-
for (int i = 0, size = operations.size(); i < size; i++) {
112-
operations.get(i).execute(mNodesManager);
113-
}
114-
}
115-
116-
if (mNodesManager.hasActiveAnimations()) {
117-
mNodesManager.runUpdates(frameTimeNanos);
100+
NativeAnimatedNodesManager nodesManager = getNodesManager();
101+
if (nodesManager.hasActiveAnimations()) {
102+
nodesManager.runUpdates(frameTimeNanos);
118103
}
119104

120105
// TODO: Would be great to avoid adding this callback in case there are no active animations
@@ -130,7 +115,10 @@ protected void doFrameGuarded(final long frameTimeNanos) {
130115

131116
@Override
132117
public void initialize() {
133-
getReactApplicationContext().addLifecycleEventListener(this);
118+
ReactApplicationContext reactCtx = getReactApplicationContext();
119+
UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);
120+
reactCtx.addLifecycleEventListener(this);
121+
uiManager.addUIManagerListener(this);
134122
}
135123

136124
@Override
@@ -139,24 +127,32 @@ public void onHostResume() {
139127
}
140128

141129
@Override
142-
public void onBatchComplete() {
143-
// Note: The order of executing onBatchComplete handler (especially in terms of onBatchComplete
144-
// from the UIManagerModule) doesn't matter as we only enqueue operations for the UI thread to
145-
// be executed from here. Thanks to ReactChoreographer all the operations from here are going
146-
// to be executed *after* all the operations enqueued by UIManager as the callback type that we
147-
// use for ReactChoreographer (CallbackType.NATIVE_ANIMATED_MODULE) is run after callbacks that
148-
// UIManager uses.
149-
ArrayList<UIThreadOperation> operations = mOperations.isEmpty() ? null : mOperations;
150-
if (operations != null) {
151-
mOperations = new ArrayList<>();
152-
synchronized (mOperationsCopyLock) {
153-
if (mReadyOperations == null) {
154-
mReadyOperations = operations;
155-
} else {
156-
mReadyOperations.addAll(operations);
130+
public void willDispatchViewUpdates(final UIManagerModule uiManager) {
131+
if (mOperations.isEmpty() && mPreOperations.isEmpty()) {
132+
return;
133+
}
134+
final ArrayList<UIThreadOperation> preOperations = mPreOperations;
135+
final ArrayList<UIThreadOperation> operations = mOperations;
136+
mPreOperations = new ArrayList<>();
137+
mOperations = new ArrayList<>();
138+
uiManager.prependUIBlock(new UIBlock() {
139+
@Override
140+
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
141+
NativeAnimatedNodesManager nodesManager = getNodesManager();
142+
for (UIThreadOperation operation : preOperations) {
143+
operation.execute(nodesManager);
157144
}
158145
}
159-
}
146+
});
147+
uiManager.addUIBlock(new UIBlock() {
148+
@Override
149+
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
150+
NativeAnimatedNodesManager nodesManager = getNodesManager();
151+
for (UIThreadOperation operation : operations) {
152+
operation.execute(nodesManager);
153+
}
154+
}
155+
});
160156
}
161157

162158
@Override
@@ -174,6 +170,15 @@ public String getName() {
174170
return NAME;
175171
}
176172

173+
private NativeAnimatedNodesManager getNodesManager() {
174+
if (mNodesManager == null) {
175+
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
176+
mNodesManager = new NativeAnimatedNodesManager(uiManager);
177+
}
178+
179+
return mNodesManager;
180+
}
181+
177182
private void clearFrameCallback() {
178183
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
179184
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
@@ -186,6 +191,11 @@ private void enqueueFrameCallback() {
186191
mAnimatedFrameCallback);
187192
}
188193

194+
@VisibleForTesting
195+
public void setNodesManager(NativeAnimatedNodesManager nodesManager) {
196+
mNodesManager = nodesManager;
197+
}
198+
189199
@ReactMethod
190200
public void createAnimatedNode(final int tag, final ReadableMap config) {
191201
mOperations.add(new UIThreadOperation() {
@@ -336,6 +346,12 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
336346

337347
@ReactMethod
338348
public void disconnectAnimatedNodeFromView(final int animatedNodeTag, final int viewTag) {
349+
mPreOperations.add(new UIThreadOperation() {
350+
@Override
351+
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
352+
animatedNodesManager.restoreDefaultValues(animatedNodeTag, viewTag);
353+
}
354+
});
339355
mOperations.add(new UIThreadOperation() {
340356
@Override
341357
public void execute(NativeAnimatedNodesManager animatedNodesManager) {

ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java

+20-11
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public void createAnimatedNode(int tag, ReadableMap config) {
9292
} else if ("value".equals(type)) {
9393
node = new ValueAnimatedNode(config);
9494
} else if ("props".equals(type)) {
95-
node = new PropsAnimatedNode(config, this);
95+
node = new PropsAnimatedNode(config, this, mUIImplementation);
9696
} else if ("interpolation".equals(type)) {
9797
node = new InterpolationAnimatedNode(config);
9898
} else if ("addition".equals(type)) {
@@ -289,11 +289,7 @@ public void connectAnimatedNodeToView(int animatedNodeTag, int viewTag) {
289289
"of type " + PropsAnimatedNode.class.getName());
290290
}
291291
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
292-
if (propsAnimatedNode.mConnectedViewTag != -1) {
293-
throw new JSApplicationIllegalArgumentException("Animated node " + animatedNodeTag + " is " +
294-
"already attached to a view");
295-
}
296-
propsAnimatedNode.mConnectedViewTag = viewTag;
292+
propsAnimatedNode.connectToView(viewTag);
297293
mUpdatedNodes.put(animatedNodeTag, node);
298294
}
299295

@@ -308,11 +304,24 @@ public void disconnectAnimatedNodeFromView(int animatedNodeTag, int viewTag) {
308304
"of type " + PropsAnimatedNode.class.getName());
309305
}
310306
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
311-
if (propsAnimatedNode.mConnectedViewTag != viewTag) {
312-
throw new JSApplicationIllegalArgumentException("Attempting to disconnect view that has " +
313-
"not been connected with the given animated node");
307+
propsAnimatedNode.disconnectFromView(viewTag);
308+
}
309+
310+
public void restoreDefaultValues(int animatedNodeTag, int viewTag) {
311+
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
312+
// Restoring default values needs to happen before UIManager operations so it is
313+
// possible the node hasn't been created yet if it is being connected and
314+
// disconnected in the same batch. In that case we don't need to restore
315+
// default values since it will never actually update the view.
316+
if (node == null) {
317+
return;
314318
}
315-
propsAnimatedNode.mConnectedViewTag = -1;
319+
if (!(node instanceof PropsAnimatedNode)) {
320+
throw new JSApplicationIllegalArgumentException("Animated node connected to view should be" +
321+
"of type " + PropsAnimatedNode.class.getName());
322+
}
323+
PropsAnimatedNode propsAnimatedNode = (PropsAnimatedNode) node;
324+
propsAnimatedNode.restoreDefaultValues();
316325
}
317326

318327
public void addAnimatedEventToView(int viewTag, String eventName, ReadableMap eventMapping) {
@@ -513,7 +522,7 @@ private void updateNodes(List<AnimatedNode> nodes) {
513522
if (nextNode instanceof PropsAnimatedNode) {
514523
// Send property updates to native view manager
515524
try {
516-
((PropsAnimatedNode) nextNode).updateView(mUIImplementation);
525+
((PropsAnimatedNode) nextNode).updateView();
517526
} catch (IllegalViewOperationException e) {
518527
// An exception is thrown if the view hasn't been created yet. This can happen because views are
519528
// created in batches. If this particular view didn't make it into a batch yet, the view won't

ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java

+50-18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package com.facebook.react.animated;
1111

12+
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
1213
import com.facebook.react.bridge.JavaOnlyMap;
1314
import com.facebook.react.bridge.ReadableMap;
1415
import com.facebook.react.bridge.ReadableMapKeySetIterator;
@@ -27,47 +28,78 @@
2728
*/
2829
/*package*/ class PropsAnimatedNode extends AnimatedNode {
2930

30-
/*package*/ int mConnectedViewTag = -1;
31-
31+
private int mConnectedViewTag = -1;
3232
private final NativeAnimatedNodesManager mNativeAnimatedNodesManager;
33-
private final Map<String, Integer> mPropMapping;
33+
private final UIImplementation mUIImplementation;
34+
private final Map<String, Integer> mPropNodeMapping;
35+
// This is the backing map for `mDiffMap` we can mutate this to update it instead of having to
36+
// create a new one for each update.
37+
private final JavaOnlyMap mPropMap;
38+
private final ReactStylesDiffMap mDiffMap;
3439

35-
PropsAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) {
40+
PropsAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager, UIImplementation uiImplementation) {
3641
ReadableMap props = config.getMap("props");
3742
ReadableMapKeySetIterator iter = props.keySetIterator();
38-
mPropMapping = new HashMap<>();
43+
mPropNodeMapping = new HashMap<>();
3944
while (iter.hasNextKey()) {
4045
String propKey = iter.nextKey();
4146
int nodeIndex = props.getInt(propKey);
42-
mPropMapping.put(propKey, nodeIndex);
47+
mPropNodeMapping.put(propKey, nodeIndex);
4348
}
49+
mPropMap = new JavaOnlyMap();
50+
mDiffMap = new ReactStylesDiffMap(mPropMap);
4451
mNativeAnimatedNodesManager = nativeAnimatedNodesManager;
52+
mUIImplementation = uiImplementation;
53+
}
54+
55+
public void connectToView(int viewTag) {
56+
if (mConnectedViewTag != -1) {
57+
throw new JSApplicationIllegalArgumentException("Animated node " + mTag + " is " +
58+
"already attached to a view");
59+
}
60+
mConnectedViewTag = viewTag;
61+
}
62+
63+
public void disconnectFromView(int viewTag) {
64+
if (mConnectedViewTag != viewTag) {
65+
throw new JSApplicationIllegalArgumentException("Attempting to disconnect view that has " +
66+
"not been connected with the given animated node");
67+
}
68+
69+
mConnectedViewTag = -1;
4570
}
4671

47-
public final void updateView(UIImplementation uiImplementation) {
72+
public void restoreDefaultValues() {
73+
ReadableMapKeySetIterator it = mPropMap.keySetIterator();
74+
while(it.hasNextKey()) {
75+
mPropMap.putNull(it.nextKey());
76+
}
77+
78+
mUIImplementation.synchronouslyUpdateViewOnUIThread(
79+
mConnectedViewTag,
80+
mDiffMap);
81+
}
82+
83+
public final void updateView() {
4884
if (mConnectedViewTag == -1) {
49-
throw new IllegalStateException("Node has not been attached to a view");
85+
return;
5086
}
51-
JavaOnlyMap propsMap = new JavaOnlyMap();
52-
for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) {
87+
for (Map.Entry<String, Integer> entry : mPropNodeMapping.entrySet()) {
5388
@Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
5489
if (node == null) {
5590
throw new IllegalArgumentException("Mapped property node does not exists");
5691
} else if (node instanceof StyleAnimatedNode) {
57-
((StyleAnimatedNode) node).collectViewUpdates(propsMap);
92+
((StyleAnimatedNode) node).collectViewUpdates(mPropMap);
5893
} else if (node instanceof ValueAnimatedNode) {
59-
propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
94+
mPropMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
6095
} else {
6196
throw new IllegalArgumentException("Unsupported type of node used in property node " +
6297
node.getClass());
6398
}
6499
}
65-
// TODO: Reuse propsMap and stylesDiffMap objects - note that in subsequent animation steps
66-
// for a given node most of the time we will be creating the same set of props (just with
67-
// different values). We can take advantage on that and optimize the way we allocate property
68-
// maps (we also know that updating view props doesn't retain a reference to the styles object).
69-
uiImplementation.synchronouslyUpdateViewOnUIThread(
100+
101+
mUIImplementation.synchronouslyUpdateViewOnUIThread(
70102
mConnectedViewTag,
71-
new ReactStylesDiffMap(propsMap));
103+
mDiffMap);
72104
}
73105
}

ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java

+4
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,10 @@ public void addUIBlock(UIBlock block) {
881881
mOperationsQueue.enqueueUIBlock(block);
882882
}
883883

884+
public void prependUIBlock(UIBlock block) {
885+
mOperationsQueue.prependUIBlock(block);
886+
}
887+
884888
public int resolveRootTagFromReactTag(int reactTag) {
885889
if (mShadowNodeRegistry.isRootNode(reactTag)) {
886890
return reactTag;

0 commit comments

Comments
 (0)