diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index acb994a9b7d980..41d0e52e60da26 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -116,6 +116,13 @@ const API = { invariant(NativeAnimatedModule, 'Native animated module is not available'); NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag); }, + restoreDefaultValues: function(nodeTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + // Backwards compat with older native runtimes, can be removed later. + if (NativeAnimatedModule.restoreDefaultValues != null) { + NativeAnimatedModule.restoreDefaultValues(nodeTag); + } + }, dropAnimatedNode: function(tag: number): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); NativeAnimatedModule.dropAnimatedNode(tag); diff --git a/Libraries/Animated/src/NativeAnimatedModule.js b/Libraries/Animated/src/NativeAnimatedModule.js index 9c3ef2d9215643..aa9e1257d2ce66 100644 --- a/Libraries/Animated/src/NativeAnimatedModule.js +++ b/Libraries/Animated/src/NativeAnimatedModule.js @@ -45,6 +45,7 @@ export interface Spec extends TurboModule { +extractAnimatedNodeOffset: (nodeTag: number) => void; +connectAnimatedNodeToView: (nodeTag: number, viewTag: number) => void; +disconnectAnimatedNodeFromView: (nodeTag: number, viewTag: number) => void; + +restoreDefaultValues: (nodeTag: number) => void; +dropAnimatedNode: (tag: number) => void; +addAnimatedEventToView: ( viewTag: number, diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js index b6e34047f6cf93..c511638be6add8 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -46,6 +46,7 @@ describe('Native Animated', () => { extractAnimatedNodeOffset: jest.fn(), flattenAnimatedNodeOffset: jest.fn(), removeAnimatedEventFromView: jest.fn(), + restoreDefaultValues: jest.fn(), setAnimatedNodeOffset: jest.fn(), setAnimatedNodeValue: jest.fn(), startAnimatingNode: jest.fn(), @@ -837,4 +838,25 @@ describe('Native Animated', () => { expect(NativeAnimatedModule.stopAnimation).toBeCalledWith(animationId); }); }); + + describe('Animated Components', () => { + it('Should restore default values on prop updates only', () => { + const opacity = new Animated.Value(0); + opacity.__makeNative(); + + const root = TestRenderer.create(); + expect(NativeAnimatedModule.restoreDefaultValues).not.toHaveBeenCalled(); + + root.update(); + expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledWith( + expect.any(Number), + ); + + root.unmount(); + // Make sure it doesn't get called on unmount. + expect(NativeAnimatedModule.restoreDefaultValues).toHaveBeenCalledTimes( + 1, + ); + }); + }); }); diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js index 4a47ce6203e790..d803781e158194 100644 --- a/Libraries/Animated/src/createAnimatedComponent.js +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -111,7 +111,10 @@ function createAnimatedComponent( // This way the intermediate state isn't to go to 0 and trigger // this expensive recursive detaching to then re-attach everything on // the very next operation. - oldPropsAnimated && oldPropsAnimated.__detach(); + if (oldPropsAnimated) { + oldPropsAnimated.__restoreDefaultValues(); + oldPropsAnimated.__detach(); + } } _setComponentRef = setAndForwardRef({ diff --git a/Libraries/Animated/src/nodes/AnimatedProps.js b/Libraries/Animated/src/nodes/AnimatedProps.js index b45073f8014cf2..be630133f1a471 100644 --- a/Libraries/Animated/src/nodes/AnimatedProps.js +++ b/Libraries/Animated/src/nodes/AnimatedProps.js @@ -147,6 +147,16 @@ class AnimatedProps extends AnimatedNode { ); } + __restoreDefaultValues(): void { + // When using the native driver, view properties need to be restored to + // their default values manually since react no longer tracks them. This + // is needed to handle cases where a prop driven by native animated is removed + // after having been changed natively by an animation. + if (this.__isNative) { + NativeAnimatedHelper.API.restoreDefaultValues(this.__getNativeTag()); + } + } + __getNativeConfig(): Object { const propsConfig = {}; for (const propKey in this._props) { diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm index 2460facf8a0605..36330dc0cf04d8 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm @@ -281,6 +281,10 @@ + (RCTManagedPointer *)JS_NativeAnimatedModule_EventMapping:(id)json return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "disconnectAnimatedNodeFromView", @selector(disconnectAnimatedNodeFromView:viewTag:), args, count); } + static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_restoreDefaultValues(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "restoreDefaultValues", @selector(restoreDefaultValues:), args, count); + } + static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_dropAnimatedNode(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "dropAnimatedNode", @selector(dropAnimatedNode:), args, count); } @@ -344,6 +348,9 @@ + (RCTManagedPointer *)JS_NativeAnimatedModule_EventMapping:(id)json methodMap_["disconnectAnimatedNodeFromView"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_disconnectAnimatedNodeFromView}; + methodMap_["restoreDefaultValues"] = MethodMetadata {1, __hostFunction_NativeAnimatedModuleSpecJSI_restoreDefaultValues}; + + methodMap_["dropAnimatedNode"] = MethodMetadata {1, __hostFunction_NativeAnimatedModuleSpecJSI_dropAnimatedNode}; diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index 51fe41d78de983..1af255238dd867 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -290,6 +290,7 @@ namespace JS { viewTag:(double)viewTag; - (void)disconnectAnimatedNodeFromView:(double)nodeTag viewTag:(double)viewTag; +- (void)restoreDefaultValues:(double)nodeTag; - (void)dropAnimatedNode:(double)tag; - (void)addAnimatedEventToView:(double)viewTag eventName:(NSString *)eventName diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm index 6b7332c243ab71..ee4beec587d88a 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm @@ -160,14 +160,18 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView:(double)nodeTag viewTag:(double)viewTag) { - [self addPreOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager restoreDefaultValues:[NSNumber numberWithDouble:nodeTag]]; - }]; [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager disconnectAnimatedNodeFromView:[NSNumber numberWithDouble:nodeTag] viewTag:[NSNumber numberWithDouble:viewTag]]; }]; } +RCT_EXPORT_METHOD(restoreDefaultValues:(double)nodeTag) +{ + [self addPreOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager restoreDefaultValues:[NSNumber numberWithDouble:nodeTag]]; + }]; +} + RCT_EXPORT_METHOD(dropAnimatedNode:(double)tag) { [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java index 9ca4222c09a9c3..91710dbc5ebe0b 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java @@ -59,6 +59,9 @@ public abstract void removeAnimatedEventFromView(double viewTag, String eventNam @ReactMethod public abstract void setAnimatedNodeOffset(double nodeTag, double offset); + @ReactMethod + public abstract void restoreDefaultValues(double nodeTag); + @ReactMethod public abstract void startAnimatingNode(double animationId, double nodeTag, ReadableMap config, Callback endCallback); diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java index 7bd186202a6681..538f7672789326 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -373,18 +373,22 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) { @ReactMethod public void disconnectAnimatedNodeFromView(final int animatedNodeTag, final int viewTag) { - mPreOperations.add( + mOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { - animatedNodesManager.restoreDefaultValues(animatedNodeTag, viewTag); + animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag); } }); - mOperations.add( + } + + @ReactMethod + public void restoreDefaultValues(final int animatedNodeTag) { + mPreOperations.add( new UIThreadOperation() { @Override public void execute(NativeAnimatedNodesManager animatedNodesManager) { - animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag); + animatedNodesManager.restoreDefaultValues(animatedNodeTag); } }); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index cff5d610d07725..9ce9e66470ccfb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -318,7 +318,7 @@ public void disconnectAnimatedNodeFromView(int animatedNodeTag, int viewTag) { propsAnimatedNode.disconnectFromView(viewTag); } - public void restoreDefaultValues(int animatedNodeTag, int viewTag) { + 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 diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index 9ce85cac0d9820..0a854969495375 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -981,7 +981,7 @@ public void testRestoreDefaultProps() { assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(0); reset(mUIManagerMock); - mNativeAnimatedNodesManager.restoreDefaultValues(propsNodeTag, viewTag); + mNativeAnimatedNodesManager.restoreDefaultValues(propsNodeTag); verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().isNull("opacity")); }