Skip to content

Commit d922842

Browse files
TMaszkofacebook-github-bot
authored andcommitted
Fix: Preserve native animated value after animated component unmount (#28841)
Summary: After animation has been finished using Native driver there is no final value passed from the native to JS side. This causes a bug from #28114. This PR solves this problem in the same way as `react-native-reanimated` library. When detaching it is calling native side to get the last value from Animated node and stores it on the JS side. Preserving animated value even if animation was using `useNativeDriver: true` Fixes #28114 ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. For an example, see: https://github.com/facebook/react-native/wiki/Changelog --> [Internal] [Fixed] - Save native Animated node value on JS side in detach phase Pull Request resolved: #28841 Test Plan: Unit tests for added getValue method passed. Green CI Reviewed By: mdvacca Differential Revision: D22211499 Pulled By: JoshuaGross fbshipit-source-id: 9a3a98a9f9a8536fe2c8764f667cdabe1f6ba82a
1 parent 0fda91f commit d922842

File tree

16 files changed

+236
-1
lines changed

16 files changed

+236
-1
lines changed

Libraries/Animated/src/NativeAnimatedHelper.js

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ const API = {
3838
enableQueue: function(): void {
3939
queueConnections = true;
4040
},
41+
getValue: function(
42+
tag: number,
43+
saveValueCallback: (value: number) => void,
44+
): void {
45+
invariant(NativeAnimatedModule, 'Native animated module is not available');
46+
if (NativeAnimatedModule.getValue) {
47+
NativeAnimatedModule.getValue(tag, saveValueCallback);
48+
}
49+
},
4150
disableQueue: function(): void {
4251
invariant(NativeAnimatedModule, 'Native animated module is not available');
4352
queueConnections = false;

Libraries/Animated/src/NativeAnimatedModule.js

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry';
1515

1616
type EndResult = {finished: boolean, ...};
1717
type EndCallback = (result: EndResult) => void;
18+
type SaveValueCallback = (value: number) => void;
1819

1920
export type EventMapping = {|
2021
nativeEventPath: Array<string>,
@@ -28,6 +29,7 @@ export type AnimatingNodeConfig = Object;
2829

2930
export interface Spec extends TurboModule {
3031
+createAnimatedNode: (tag: number, config: AnimatedNodeConfig) => void;
32+
+getValue: (tag: number, saveValueCallback: SaveValueCallback) => void;
3133
+startListeningToAnimatedNodeValue: (tag: number) => void;
3234
+stopListeningToAnimatedNodeValue: (tag: number) => void;
3335
+connectAnimatedNodes: (parentTag: number, childTag: number) => void;

Libraries/Animated/src/__tests__/AnimatedNative-test.js

+21
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('Native Animated', () => {
3636

3737
beforeEach(() => {
3838
Object.assign(NativeAnimatedModule, {
39+
getValue: jest.fn(),
3940
addAnimatedEventToView: jest.fn(),
4041
connectAnimatedNodes: jest.fn(),
4142
connectAnimatedNodeToView: jest.fn(),
@@ -115,6 +116,26 @@ describe('Native Animated', () => {
115116
);
116117
});
117118

119+
it('should save value on unmount', () => {
120+
NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => {
121+
saveCallback(1);
122+
});
123+
const opacity = new Animated.Value(0);
124+
125+
opacity.__makeNative();
126+
127+
const root = TestRenderer.create(<Animated.View style={{opacity}} />);
128+
const tag = opacity.__getNativeTag();
129+
130+
root.unmount();
131+
132+
expect(NativeAnimatedModule.getValue).toBeCalledWith(
133+
tag,
134+
expect.any(Function),
135+
);
136+
expect(opacity.__getValue()).toBe(1);
137+
});
138+
118139
it('should extract offset', () => {
119140
const opacity = new Animated.Value(0);
120141
opacity.__makeNative();

Libraries/Animated/src/nodes/AnimatedValue.js

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ class AnimatedValue extends AnimatedWithChildren {
8686
}
8787

8888
__detach() {
89+
if (this.__isNative) {
90+
NativeAnimatedAPI.getValue(this.__getNativeTag(), value => {
91+
this._value = value;
92+
});
93+
}
8994
this.stopAnimation();
9095
super.__detach();
9196
}

Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm

+7
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ + (RCTManagedPointer *)JS_NativeAnimatedModule_EventMapping:(id)json
233233
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "createAnimatedNode", @selector(createAnimatedNode:config:), args, count);
234234
}
235235

236+
static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_getValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
237+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "getValue", @selector(getValue:saveValueCallback:), args, count);
238+
}
239+
236240
static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
237241
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "startListeningToAnimatedNodeValue", @selector(startListeningToAnimatedNodeValue:), args, count);
238242
}
@@ -312,6 +316,9 @@ + (RCTManagedPointer *)JS_NativeAnimatedModule_EventMapping:(id)json
312316
methodMap_["createAnimatedNode"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_createAnimatedNode};
313317

314318

319+
methodMap_["getValue"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_getValue};
320+
321+
315322
methodMap_["startListeningToAnimatedNodeValue"] = MethodMetadata {1, __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue};
316323

317324

Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h

+2
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ namespace JS {
271271

272272
- (void)createAnimatedNode:(double)tag
273273
config:(NSDictionary *)config;
274+
- (void)getValue:(double)tag
275+
saveValueCallback:(RCTResponseSenderBlock)saveValueCallback;
274276
- (void)startListeningToAnimatedNodeValue:(double)tag;
275277
- (void)stopListeningToAnimatedNodeValue:(double)tag;
276278
- (void)connectAnimatedNodes:(double)parentTag

Libraries/NativeAnimation/RCTNativeAnimatedModule.mm

+6-1
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ - (void)setBridge:(RCTBridge *)bridge
216216
}];
217217
}
218218

219+
RCT_EXPORT_METHOD(getValue:(double)nodeTag saveCallback:(RCTResponseSenderBlock)saveCallback) {
220+
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
221+
[nodesManager getValue:[NSNumber numberWithDouble:nodeTag] saveCallback:saveCallback];
222+
}];
223+
}
224+
219225
#pragma mark -- Batch handling
220226

221227
- (void)addOperationBlock:(AnimatedOperation)operation
@@ -300,7 +306,6 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)uiManager
300306
operation(self->_nodesManager);
301307
}
302308
}];
303-
304309
[uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
305310
for (AnimatedOperation operation in operations) {
306311
operation(self->_nodesManager);

Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
- (BOOL)isNodeManagedByFabric:(nonnull NSNumber *)tag;
2323

24+
- (void)getValue:(nonnull NSNumber *)nodeTag
25+
saveCallback:(nullable RCTResponseSenderBlock)saveCallback;
26+
2427
// graph
2528

2629
- (void)createAnimatedNode:(nonnull NSNumber *)tag

Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m

+11
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,17 @@ - (void)extractAnimatedNodeOffset:(nonnull NSNumber *)nodeTag
242242
[valueNode extractOffset];
243243
}
244244

245+
- (void)getValue:(NSNumber *)nodeTag saveCallback:(RCTResponseSenderBlock)saveCallback
246+
{
247+
RCTAnimatedNode *node = _animationNodes[nodeTag];
248+
if (![node isKindOfClass:[RCTValueAnimatedNode class]]) {
249+
RCTLogError(@"Not a value node.");
250+
return;
251+
}
252+
RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)node;;
253+
saveCallback(@[@(valueNode.value)]);
254+
}
255+
245256
#pragma mark -- Drivers
246257

247258
- (void)startAnimatingNode:(nonnull NSNumber *)animationId

RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m

+17
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,23 @@ - (void)testNativeAnimatedEventDoNotUpdate
864864
[_uiManager verify];
865865
}
866866

867+
- (void) testGetValue
868+
{
869+
__block NSInteger saveValueCallbackCalls = 0;
870+
NSNumber *nodeTag = @100;
871+
[_nodesManager createAnimatedNode:nodeTag
872+
config:@{@"type": @"value", @"value": @1, @"offset": @0}];
873+
RCTResponseSenderBlock saveValueCallback = ^(NSArray *response) {
874+
saveValueCallbackCalls++;
875+
XCTAssertEqualObjects(response, @[@1]);
876+
};
877+
878+
XCTAssertEqual(saveValueCallbackCalls, 0);
879+
880+
[_nodesManager getValue:nodeTag saveCallback:saveValueCallback];
881+
XCTAssertEqual(saveValueCallbackCalls, 1);
882+
}
883+
867884
/**
868885
* Creates a following graph of nodes:
869886
* Value(3, initialValue) ----> Style(4) ---> Props(5) ---> View(viewTag)

RNTester/js/examples/Animated/AnimatedExample.js

+109
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,115 @@ exports.examples = [
335335
);
336336
},
337337
},
338+
{
339+
title: 'Moving box example',
340+
description: ('Click arrow buttons to move the box.' +
341+
'Then hide the box and reveal it again.' +
342+
'After that the box position will reset to initial position.': string),
343+
render: (): React.Node => {
344+
const containerWidth = 200;
345+
const boxSize = 50;
346+
347+
const movingBoxStyles = StyleSheet.create({
348+
container: {
349+
display: 'flex',
350+
alignItems: 'center',
351+
flexDirection: 'column',
352+
backgroundColor: '#fff',
353+
padding: 30,
354+
},
355+
boxContainer: {
356+
backgroundColor: '#d3d3d3',
357+
height: boxSize,
358+
width: containerWidth,
359+
},
360+
box: {
361+
width: boxSize,
362+
height: boxSize,
363+
margin: 0,
364+
},
365+
buttonsContainer: {
366+
marginTop: 20,
367+
display: 'flex',
368+
flexDirection: 'row',
369+
justifyContent: 'space-between',
370+
width: containerWidth,
371+
},
372+
});
373+
type Props = $ReadOnly<{||}>;
374+
type State = {|boxVisible: boolean|};
375+
376+
class MovingBoxExample extends React.Component<Props, State> {
377+
x: Animated.Value;
378+
constructor(props) {
379+
super(props);
380+
this.x = new Animated.Value(0);
381+
this.state = {
382+
boxVisible: true,
383+
};
384+
}
385+
386+
render() {
387+
const {boxVisible} = this.state;
388+
const toggleText = boxVisible ? 'Hide' : 'Show';
389+
return (
390+
<View style={movingBoxStyles.container}>
391+
{this.renderBox()}
392+
<View style={movingBoxStyles.buttonsContainer}>
393+
<RNTesterButton onPress={() => this.moveTo(0)}>
394+
{'<-'}
395+
</RNTesterButton>
396+
<RNTesterButton onPress={this.toggleVisibility}>
397+
{toggleText}
398+
</RNTesterButton>
399+
<RNTesterButton
400+
onPress={() => this.moveTo(containerWidth - boxSize)}>
401+
{'->'}
402+
</RNTesterButton>
403+
</View>
404+
</View>
405+
);
406+
}
407+
408+
renderBox = () => {
409+
if (this.state.boxVisible) {
410+
const horizontalLocation = {transform: [{translateX: this.x}]};
411+
return (
412+
<View style={movingBoxStyles.boxContainer}>
413+
<Animated.View
414+
style={[
415+
styles.content,
416+
movingBoxStyles.box,
417+
horizontalLocation,
418+
]}
419+
/>
420+
</View>
421+
);
422+
} else {
423+
return (
424+
<View style={movingBoxStyles.boxContainer}>
425+
<Text>The box view is not being rendered</Text>
426+
</View>
427+
);
428+
}
429+
};
430+
431+
moveTo = x => {
432+
Animated.timing(this.x, {
433+
toValue: x,
434+
duration: 1000,
435+
useNativeDriver: true,
436+
}).start();
437+
};
438+
439+
toggleVisibility = () => {
440+
const {boxVisible} = this.state;
441+
this.setState({boxVisible: !boxVisible});
442+
};
443+
}
444+
return <MovingBoxExample />;
445+
},
446+
},
338447
{
339448
title: 'Continuous Interactions',
340449
description: ('Gesture events, chaining, 2D ' +

ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAnimatedModuleSpec.java

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public abstract void removeAnimatedEventFromView(double viewTag, String eventNam
6666
public abstract void startAnimatingNode(double animationId, double nodeTag, ReadableMap config,
6767
Callback endCallback);
6868

69+
@ReactMethod
70+
public abstract void getValue(double tag, Callback saveValueCallback);
71+
6972
@ReactMethod
7073
public abstract void stopListeningToAnimatedNodeValue(double tag);
7174

ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ namespace facebook {
186186
return static_cast<JavaTurboModule&>(turboModule).invokeJavaMethod(rt, VoidKind, "createAnimatedNode", "(DLcom/facebook/react/bridge/ReadableMap;)V", args, count);
187187
}
188188

189+
static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_getValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
190+
return static_cast<JavaTurboModule&>(turboModule).invokeJavaMethod(rt, VoidKind, "getValue", "(DLcom/facebook/react/bridge/Callback;)V", args, count);
191+
}
192+
189193
static facebook::jsi::Value __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
190194
return static_cast<JavaTurboModule&>(turboModule).invokeJavaMethod(rt, VoidKind, "startListeningToAnimatedNodeValue", "(D)V", args, count);
191195
}
@@ -265,6 +269,9 @@ namespace facebook {
265269
methodMap_["createAnimatedNode"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_createAnimatedNode};
266270

267271

272+
methodMap_["getValue"] = MethodMetadata {2, __hostFunction_NativeAnimatedModuleSpecJSI_getValue};
273+
274+
268275
methodMap_["startListeningToAnimatedNodeValue"] = MethodMetadata {1, __hostFunction_NativeAnimatedModuleSpecJSI_startListeningToAnimatedNodeValue};
269276

270277

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

+12
Original file line numberDiff line numberDiff line change
@@ -774,4 +774,16 @@ public void addListener(String eventName) {
774774
public void removeListeners(double count) {
775775
// iOS only
776776
}
777+
778+
@Override
779+
public void getValue(final double animatedValueNodeTagDouble, final Callback callback) {
780+
final int animatedValueNodeTag = (int) animatedValueNodeTagDouble;
781+
mOperations.add(
782+
new UIThreadOperation() {
783+
@Override
784+
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
785+
animatedNodesManager.getValue(animatedValueNodeTag, callback);
786+
}
787+
});
788+
}
777789
}

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

+9
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,15 @@ public void disconnectAnimatedNodeFromView(int animatedNodeTag, int viewTag) {
352352
propsAnimatedNode.disconnectFromView(viewTag);
353353
}
354354

355+
public void getValue(int tag, Callback callback) {
356+
AnimatedNode node = mAnimatedNodes.get(tag);
357+
if (node == null || !(node instanceof ValueAnimatedNode)) {
358+
throw new JSApplicationIllegalArgumentException(
359+
"Animated node with tag " + tag + " does not exists or is not a 'value' node");
360+
}
361+
callback.invoke(((ValueAnimatedNode) node).getValue());
362+
}
363+
355364
public void restoreDefaultValues(int animatedNodeTag) {
356365
AnimatedNode node = mAnimatedNodes.get(animatedNodeTag);
357366
// Restoring default values needs to happen before UIManager operations so it is

ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java

+13
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,19 @@ public void testHandleStoppingAnimation() {
860860
verifyNoMoreInteractions(animationCallback);
861861
}
862862

863+
@Test
864+
public void testGetValue() {
865+
int tag = 1;
866+
mNativeAnimatedNodesManager.createAnimatedNode(
867+
tag, JavaOnlyMap.of("type", "value", "value", 1d, "offset", 0d));
868+
869+
Callback saveValueCallbackMock = mock(Callback.class);
870+
871+
mNativeAnimatedNodesManager.getValue(tag, saveValueCallbackMock);
872+
873+
verify(saveValueCallbackMock, times(1)).invoke(1d);
874+
}
875+
863876
@Test
864877
public void testInterpolationNode() {
865878
mNativeAnimatedNodesManager.createAnimatedNode(

0 commit comments

Comments
 (0)