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 https://github.com/facebook/react-native/pull/6641 Differential Revision: D3195963 fb-gh-sync-id: bb1e1a36821a0e071ad0e7d0fa99ce0d6b088b0a fbshipit-source-id: bb1e1a36821a0e071ad0e7d0fa99ce0d6b088b0a
This commit is contained in:
parent
64d5da7754
commit
b5375bdc09
|
@ -952,6 +952,12 @@ class AnimatedAddition extends AnimatedWithChildren {
|
|||
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
|
||||
}
|
||||
|
||||
__makeNative() {
|
||||
super.__makeNative();
|
||||
this._a.__makeNative();
|
||||
this._b.__makeNative();
|
||||
}
|
||||
|
||||
__getValue(): number {
|
||||
return this._a.__getValue() + this._b.__getValue();
|
||||
}
|
||||
|
@ -968,6 +974,14 @@ class AnimatedAddition extends AnimatedWithChildren {
|
|||
__detach(): void {
|
||||
this._a.__removeChild(this);
|
||||
this._b.__removeChild(this);
|
||||
super.__detach();
|
||||
}
|
||||
|
||||
__getNativeConfig(): any {
|
||||
return {
|
||||
type: 'addition',
|
||||
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -90,6 +90,40 @@ describe('Animated', () => {
|
|||
.toBeCalledWith(jasmine.any(Number), { type: 'props', props: { style: jasmine.any(Number) }});
|
||||
});
|
||||
|
||||
it('sends a valid graph description for Animated.add nodes', () => {
|
||||
var first = new Animated.Value(1);
|
||||
var second = new Animated.Value(2);
|
||||
|
||||
var c = new Animated.View();
|
||||
c.props = {
|
||||
style: {
|
||||
opacity: Animated.add(first, second),
|
||||
},
|
||||
};
|
||||
c.componentWillMount();
|
||||
|
||||
Animated.timing(first, {toValue: 2, duration: 1000, useNativeDriver: true}).start();
|
||||
Animated.timing(second, {toValue: 3, duration: 1000, useNativeDriver: true}).start();
|
||||
|
||||
var nativeAnimatedModule = require('NativeModules').NativeAnimatedModule;
|
||||
expect(nativeAnimatedModule.createAnimatedNode)
|
||||
.toBeCalledWith(jasmine.any(Number), { type: 'addition', input: jasmine.any(Array) });
|
||||
var additionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter(
|
||||
(call) => call[1].type === 'addition'
|
||||
);
|
||||
expect(additionCalls.length).toBe(1);
|
||||
var additionCall = additionCalls[0];
|
||||
var additionNodeTag = additionCall[0];
|
||||
var additionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter(
|
||||
(call) => call[1] === additionNodeTag
|
||||
);
|
||||
expect(additionConnectionCalls.length).toBe(2);
|
||||
expect(nativeAnimatedModule.createAnimatedNode)
|
||||
.toBeCalledWith(additionCall[1].input[0], { type: 'value', value: 1 });
|
||||
expect(nativeAnimatedModule.createAnimatedNode)
|
||||
.toBeCalledWith(additionCall[1].input[1], { type: 'value', value: 2 });
|
||||
});
|
||||
|
||||
it('sends a valid timing animation description', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: true}).start();
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.facebook.react.animated;
|
||||
|
||||
import com.facebook.react.bridge.JSApplicationCausedNativeException;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
/**
|
||||
* Animated node that plays a role of value aggregator. It takes two or more value nodes as an input
|
||||
* and outputs a sum of values outputted by those nodes.
|
||||
*/
|
||||
/*package*/ class AdditionAnimatedNode extends ValueAnimatedNode {
|
||||
|
||||
private final NativeAnimatedNodesManager mNativeAnimatedNodesManager;
|
||||
private final int[] mInputNodes;
|
||||
|
||||
public AdditionAnimatedNode(
|
||||
ReadableMap config,
|
||||
NativeAnimatedNodesManager nativeAnimatedNodesManager) {
|
||||
mNativeAnimatedNodesManager = nativeAnimatedNodesManager;
|
||||
ReadableArray inputNodes = config.getArray("input");
|
||||
mInputNodes = new int[inputNodes.size()];
|
||||
for (int i = 0; i < mInputNodes.length; i++) {
|
||||
mInputNodes[i] = inputNodes.getInt(i);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
mValue = 0;
|
||||
for (int i = 0; i < mInputNodes.length; i++) {
|
||||
AnimatedNode animatedNode = mNativeAnimatedNodesManager.getNodeById(mInputNodes[i]);
|
||||
if (animatedNode != null && animatedNode instanceof ValueAnimatedNode) {
|
||||
mValue += ((ValueAnimatedNode) animatedNode).mValue;
|
||||
} else {
|
||||
throw new JSApplicationCausedNativeException("Illegal node ID set as an input for " +
|
||||
"Animated.Add node");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,6 +72,8 @@ import javax.annotation.Nullable;
|
|||
mUpdatedNodes.add(node);
|
||||
} else if ("props".equals(type)) {
|
||||
node = new PropsAnimatedNode(config, this);
|
||||
} else if ("addition".equals(type)) {
|
||||
node = new AdditionAnimatedNode(config, this);
|
||||
} else {
|
||||
throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type);
|
||||
}
|
||||
|
|
|
@ -15,11 +15,15 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
* Basic type of animated node that maps directly from {@code Animated.Value(x)} of Animated.js
|
||||
* library.
|
||||
*/
|
||||
class ValueAnimatedNode extends AnimatedNode {
|
||||
/*package*/ class ValueAnimatedNode extends AnimatedNode {
|
||||
|
||||
/*package*/ double mValue = Double.NaN;
|
||||
|
||||
ValueAnimatedNode(ReadableMap config) {
|
||||
public ValueAnimatedNode() {
|
||||
// empty constructor that can be used by subclasses
|
||||
}
|
||||
|
||||
public ValueAnimatedNode(ReadableMap config) {
|
||||
mValue = config.getDouble("value");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,4 +165,180 @@ public class NativeAnimatedNodeTraversalTest {
|
|||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verifyNoMoreInteractions(animationCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a following graph of nodes:
|
||||
* Value(1, firstValue) ----> Add(3) ---> Style(4) ---> Props(5) ---> View(viewTag)
|
||||
* |
|
||||
* Value(2, secondValue) --+
|
||||
*
|
||||
* Add(3) node maps to a "translateX" attribute of the Style(4) node.
|
||||
*/
|
||||
private void createAnimatedGraphWithAdditionNode(
|
||||
int viewTag,
|
||||
double firstValue,
|
||||
double secondValue) {
|
||||
mNativeAnimatedNodesManager.createAnimatedNode(
|
||||
1,
|
||||
JavaOnlyMap.of("type", "value", "value", 100d));
|
||||
mNativeAnimatedNodesManager.createAnimatedNode(
|
||||
2,
|
||||
JavaOnlyMap.of("type", "value", "value", 1000d));
|
||||
|
||||
mNativeAnimatedNodesManager.createAnimatedNode(
|
||||
3,
|
||||
JavaOnlyMap.of("type", "addition", "input", JavaOnlyArray.of(1, 2)));
|
||||
|
||||
mNativeAnimatedNodesManager.createAnimatedNode(
|
||||
4,
|
||||
JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("translateX", 3)));
|
||||
mNativeAnimatedNodesManager.createAnimatedNode(
|
||||
5,
|
||||
JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 4)));
|
||||
mNativeAnimatedNodesManager.connectAnimatedNodes(1, 3);
|
||||
mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3);
|
||||
mNativeAnimatedNodesManager.connectAnimatedNodes(3, 4);
|
||||
mNativeAnimatedNodesManager.connectAnimatedNodes(4, 5);
|
||||
mNativeAnimatedNodesManager.connectAnimatedNodeToView(5, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionNode() {
|
||||
createAnimatedGraphWithAdditionNode(50, 100d, 1000d);
|
||||
|
||||
Callback animationCallback = mock(Callback.class);
|
||||
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||
1,
|
||||
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
|
||||
animationCallback);
|
||||
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||
2,
|
||||
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1010d),
|
||||
animationCallback);
|
||||
|
||||
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
|
||||
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock)
|
||||
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock)
|
||||
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1111d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verifyNoMoreInteractions(mUIImplementationMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that {@link NativeAnimatedNodesManager#runUpdates} updates the view correctly in case
|
||||
* when one of the addition input nodes has started animating while the other one has not.
|
||||
*
|
||||
* We expect that the output of the addition node will take the starting value of the second input
|
||||
* node even though the node hasn't been connected to an active animation driver.
|
||||
*/
|
||||
@Test
|
||||
public void testViewReceiveUpdatesIfOneOfAnimationHasntStarted() {
|
||||
createAnimatedGraphWithAdditionNode(50, 100d, 1000d);
|
||||
|
||||
// Start animating only the first addition input node
|
||||
Callback animationCallback = mock(Callback.class);
|
||||
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||
1,
|
||||
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
|
||||
animationCallback);
|
||||
|
||||
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
|
||||
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock)
|
||||
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock)
|
||||
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1101d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verifyNoMoreInteractions(mUIImplementationMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that {@link NativeAnimatedNodesManager#runUpdates} updates the view correctly in case
|
||||
* when one of the addition input nodes animation finishes before the other.
|
||||
*
|
||||
* We expect that the output of the addition node after one of the animation has finished will
|
||||
* take the last value of the animated node and the view will receive updates up until the second
|
||||
* animation is over.
|
||||
*/
|
||||
@Test
|
||||
public void testViewReceiveUpdatesWhenOneOfAnimationHasFinished() {
|
||||
createAnimatedGraphWithAdditionNode(50, 100d, 1000d);
|
||||
|
||||
Callback animationCallback = mock(Callback.class);
|
||||
|
||||
// Start animating for the first addition input node, will have 2 frames only
|
||||
JavaOnlyArray firstFrames = JavaOnlyArray.of(0d, 1d);
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||
1,
|
||||
JavaOnlyMap.of("type", "frames", "frames", firstFrames, "toValue", 200d),
|
||||
animationCallback);
|
||||
|
||||
// Start animating for the first addition input node, will have 6 frames
|
||||
JavaOnlyArray secondFrames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||
2,
|
||||
JavaOnlyMap.of("type", "frames", "frames", secondFrames, "toValue", 1010d),
|
||||
animationCallback);
|
||||
|
||||
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
|
||||
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d);
|
||||
|
||||
for (int i = 1; i < secondFrames.size(); i++) {
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verify(mUIImplementationMock)
|
||||
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
|
||||
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN))
|
||||
.isEqualTo(1200d + secondFrames.getDouble(i) * 10d);
|
||||
}
|
||||
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verifyNoMoreInteractions(mUIImplementationMock);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue