Skip to content

Commit b5375bd

Browse files
kmagieraFacebook Github Bot 8
authored andcommitted
Support for Animated.add
Summary:This change adds suport native animated support for Animated.add. Animated.add lets you declare node that outputs a sum of it input nodes. **Test Plan** Play with the following playground app: https://gist.github.com/39de37faf07480fcd7d1 Run JS tests: `npm test Libraries/Animated/src/__tests__/AnimatedNative-test.js` Run java tests: `buck test ReactAndroid/src/test/java/com/facebook/react/animated` Closes facebook/react-native#6641 Differential Revision: D3195963 fb-gh-sync-id: bb1e1a36821a0e071ad0e7d0fa99ce0d6b088b0a fbshipit-source-id: bb1e1a36821a0e071ad0e7d0fa99ce0d6b088b0a
1 parent 64d5da7 commit b5375bd

6 files changed

Lines changed: 272 additions & 2 deletions

File tree

Libraries/Animated/src/AnimatedImplementation.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,12 @@ class AnimatedAddition extends AnimatedWithChildren {
952952
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
953953
}
954954

955+
__makeNative() {
956+
super.__makeNative();
957+
this._a.__makeNative();
958+
this._b.__makeNative();
959+
}
960+
955961
__getValue(): number {
956962
return this._a.__getValue() + this._b.__getValue();
957963
}
@@ -968,6 +974,14 @@ class AnimatedAddition extends AnimatedWithChildren {
968974
__detach(): void {
969975
this._a.__removeChild(this);
970976
this._b.__removeChild(this);
977+
super.__detach();
978+
}
979+
980+
__getNativeConfig(): any {
981+
return {
982+
type: 'addition',
983+
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
984+
};
971985
}
972986
}
973987

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,40 @@ describe('Animated', () => {
9090
.toBeCalledWith(jasmine.any(Number), { type: 'props', props: { style: jasmine.any(Number) }});
9191
});
9292

93+
it('sends a valid graph description for Animated.add nodes', () => {
94+
var first = new Animated.Value(1);
95+
var second = new Animated.Value(2);
96+
97+
var c = new Animated.View();
98+
c.props = {
99+
style: {
100+
opacity: Animated.add(first, second),
101+
},
102+
};
103+
c.componentWillMount();
104+
105+
Animated.timing(first, {toValue: 2, duration: 1000, useNativeDriver: true}).start();
106+
Animated.timing(second, {toValue: 3, duration: 1000, useNativeDriver: true}).start();
107+
108+
var nativeAnimatedModule = require('NativeModules').NativeAnimatedModule;
109+
expect(nativeAnimatedModule.createAnimatedNode)
110+
.toBeCalledWith(jasmine.any(Number), { type: 'addition', input: jasmine.any(Array) });
111+
var additionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter(
112+
(call) => call[1].type === 'addition'
113+
);
114+
expect(additionCalls.length).toBe(1);
115+
var additionCall = additionCalls[0];
116+
var additionNodeTag = additionCall[0];
117+
var additionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter(
118+
(call) => call[1] === additionNodeTag
119+
);
120+
expect(additionConnectionCalls.length).toBe(2);
121+
expect(nativeAnimatedModule.createAnimatedNode)
122+
.toBeCalledWith(additionCall[1].input[0], { type: 'value', value: 1 });
123+
expect(nativeAnimatedModule.createAnimatedNode)
124+
.toBeCalledWith(additionCall[1].input[1], { type: 'value', value: 2 });
125+
});
126+
93127
it('sends a valid timing animation description', () => {
94128
var anim = new Animated.Value(0);
95129
Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: true}).start();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.facebook.react.animated;
2+
3+
import com.facebook.react.bridge.JSApplicationCausedNativeException;
4+
import com.facebook.react.bridge.ReadableArray;
5+
import com.facebook.react.bridge.ReadableMap;
6+
7+
/**
8+
* Animated node that plays a role of value aggregator. It takes two or more value nodes as an input
9+
* and outputs a sum of values outputted by those nodes.
10+
*/
11+
/*package*/ class AdditionAnimatedNode extends ValueAnimatedNode {
12+
13+
private final NativeAnimatedNodesManager mNativeAnimatedNodesManager;
14+
private final int[] mInputNodes;
15+
16+
public AdditionAnimatedNode(
17+
ReadableMap config,
18+
NativeAnimatedNodesManager nativeAnimatedNodesManager) {
19+
mNativeAnimatedNodesManager = nativeAnimatedNodesManager;
20+
ReadableArray inputNodes = config.getArray("input");
21+
mInputNodes = new int[inputNodes.size()];
22+
for (int i = 0; i < mInputNodes.length; i++) {
23+
mInputNodes[i] = inputNodes.getInt(i);
24+
}
25+
}
26+
27+
@Override
28+
public void update() {
29+
mValue = 0;
30+
for (int i = 0; i < mInputNodes.length; i++) {
31+
AnimatedNode animatedNode = mNativeAnimatedNodesManager.getNodeById(mInputNodes[i]);
32+
if (animatedNode != null && animatedNode instanceof ValueAnimatedNode) {
33+
mValue += ((ValueAnimatedNode) animatedNode).mValue;
34+
} else {
35+
throw new JSApplicationCausedNativeException("Illegal node ID set as an input for " +
36+
"Animated.Add node");
37+
}
38+
}
39+
}
40+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public void createAnimatedNode(int tag, ReadableMap config) {
7272
mUpdatedNodes.add(node);
7373
} else if ("props".equals(type)) {
7474
node = new PropsAnimatedNode(config, this);
75+
} else if ("addition".equals(type)) {
76+
node = new AdditionAnimatedNode(config, this);
7577
} else {
7678
throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type);
7779
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
* Basic type of animated node that maps directly from {@code Animated.Value(x)} of Animated.js
1616
* library.
1717
*/
18-
class ValueAnimatedNode extends AnimatedNode {
18+
/*package*/ class ValueAnimatedNode extends AnimatedNode {
1919

2020
/*package*/ double mValue = Double.NaN;
2121

22-
ValueAnimatedNode(ReadableMap config) {
22+
public ValueAnimatedNode() {
23+
// empty constructor that can be used by subclasses
24+
}
25+
26+
public ValueAnimatedNode(ReadableMap config) {
2327
mValue = config.getDouble("value");
2428
}
2529
}

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

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,180 @@ public void testAnimationCallbackFinish() {
165165
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
166166
verifyNoMoreInteractions(animationCallback);
167167
}
168+
169+
/**
170+
* Creates a following graph of nodes:
171+
* Value(1, firstValue) ----> Add(3) ---> Style(4) ---> Props(5) ---> View(viewTag)
172+
* |
173+
* Value(2, secondValue) --+
174+
*
175+
* Add(3) node maps to a "translateX" attribute of the Style(4) node.
176+
*/
177+
private void createAnimatedGraphWithAdditionNode(
178+
int viewTag,
179+
double firstValue,
180+
double secondValue) {
181+
mNativeAnimatedNodesManager.createAnimatedNode(
182+
1,
183+
JavaOnlyMap.of("type", "value", "value", 100d));
184+
mNativeAnimatedNodesManager.createAnimatedNode(
185+
2,
186+
JavaOnlyMap.of("type", "value", "value", 1000d));
187+
188+
mNativeAnimatedNodesManager.createAnimatedNode(
189+
3,
190+
JavaOnlyMap.of("type", "addition", "input", JavaOnlyArray.of(1, 2)));
191+
192+
mNativeAnimatedNodesManager.createAnimatedNode(
193+
4,
194+
JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("translateX", 3)));
195+
mNativeAnimatedNodesManager.createAnimatedNode(
196+
5,
197+
JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 4)));
198+
mNativeAnimatedNodesManager.connectAnimatedNodes(1, 3);
199+
mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3);
200+
mNativeAnimatedNodesManager.connectAnimatedNodes(3, 4);
201+
mNativeAnimatedNodesManager.connectAnimatedNodes(4, 5);
202+
mNativeAnimatedNodesManager.connectAnimatedNodeToView(5, 50);
203+
}
204+
205+
@Test
206+
public void testAdditionNode() {
207+
createAnimatedGraphWithAdditionNode(50, 100d, 1000d);
208+
209+
Callback animationCallback = mock(Callback.class);
210+
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
211+
mNativeAnimatedNodesManager.startAnimatingNode(
212+
1,
213+
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
214+
animationCallback);
215+
216+
mNativeAnimatedNodesManager.startAnimatingNode(
217+
2,
218+
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1010d),
219+
animationCallback);
220+
221+
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
222+
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
223+
224+
reset(mUIImplementationMock);
225+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
226+
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
227+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
228+
229+
reset(mUIImplementationMock);
230+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
231+
verify(mUIImplementationMock)
232+
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
233+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
234+
235+
reset(mUIImplementationMock);
236+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
237+
verify(mUIImplementationMock)
238+
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
239+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1111d);
240+
241+
reset(mUIImplementationMock);
242+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
243+
verifyNoMoreInteractions(mUIImplementationMock);
244+
}
245+
246+
/**
247+
* Verifies that {@link NativeAnimatedNodesManager#runUpdates} updates the view correctly in case
248+
* when one of the addition input nodes has started animating while the other one has not.
249+
*
250+
* We expect that the output of the addition node will take the starting value of the second input
251+
* node even though the node hasn't been connected to an active animation driver.
252+
*/
253+
@Test
254+
public void testViewReceiveUpdatesIfOneOfAnimationHasntStarted() {
255+
createAnimatedGraphWithAdditionNode(50, 100d, 1000d);
256+
257+
// Start animating only the first addition input node
258+
Callback animationCallback = mock(Callback.class);
259+
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
260+
mNativeAnimatedNodesManager.startAnimatingNode(
261+
1,
262+
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
263+
animationCallback);
264+
265+
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
266+
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
267+
268+
reset(mUIImplementationMock);
269+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
270+
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
271+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
272+
273+
reset(mUIImplementationMock);
274+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
275+
verify(mUIImplementationMock)
276+
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
277+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
278+
279+
reset(mUIImplementationMock);
280+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
281+
verify(mUIImplementationMock)
282+
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
283+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1101d);
284+
285+
reset(mUIImplementationMock);
286+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
287+
verifyNoMoreInteractions(mUIImplementationMock);
288+
}
289+
290+
/**
291+
* Verifies that {@link NativeAnimatedNodesManager#runUpdates} updates the view correctly in case
292+
* when one of the addition input nodes animation finishes before the other.
293+
*
294+
* We expect that the output of the addition node after one of the animation has finished will
295+
* take the last value of the animated node and the view will receive updates up until the second
296+
* animation is over.
297+
*/
298+
@Test
299+
public void testViewReceiveUpdatesWhenOneOfAnimationHasFinished() {
300+
createAnimatedGraphWithAdditionNode(50, 100d, 1000d);
301+
302+
Callback animationCallback = mock(Callback.class);
303+
304+
// Start animating for the first addition input node, will have 2 frames only
305+
JavaOnlyArray firstFrames = JavaOnlyArray.of(0d, 1d);
306+
mNativeAnimatedNodesManager.startAnimatingNode(
307+
1,
308+
JavaOnlyMap.of("type", "frames", "frames", firstFrames, "toValue", 200d),
309+
animationCallback);
310+
311+
// Start animating for the first addition input node, will have 6 frames
312+
JavaOnlyArray secondFrames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
313+
mNativeAnimatedNodesManager.startAnimatingNode(
314+
2,
315+
JavaOnlyMap.of("type", "frames", "frames", secondFrames, "toValue", 1010d),
316+
animationCallback);
317+
318+
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
319+
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
320+
321+
reset(mUIImplementationMock);
322+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
323+
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
324+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
325+
326+
reset(mUIImplementationMock);
327+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
328+
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
329+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
330+
331+
for (int i = 1; i < secondFrames.size(); i++) {
332+
reset(mUIImplementationMock);
333+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
334+
verify(mUIImplementationMock)
335+
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
336+
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN))
337+
.isEqualTo(1200d + secondFrames.getDouble(i) * 10d);
338+
}
339+
340+
reset(mUIImplementationMock);
341+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
342+
verifyNoMoreInteractions(mUIImplementationMock);
343+
}
168344
}

0 commit comments

Comments
 (0)