/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.animated;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests the animated nodes graph traversal algorithm from {@link NativeAnimatedNodesManager}.
*/
@PrepareForTest({Arguments.class})
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class NativeAnimatedNodeTraversalTest {
private static long FRAME_LEN_NANOS = 1000000000L / 60L;
private static long INITIAL_FRAME_TIME_NANOS = 14599233201256L; /* random */
@Rule
public PowerMockRule rule = new PowerMockRule();
private long mFrameTimeNanos;
private UIManagerModule mUIManagerMock;
private UIImplementation mUIImplementationMock;
private EventDispatcher mEventDispatcherMock;
private NativeAnimatedNodesManager mNativeAnimatedNodesManager;
private long nextFrameTime() {
return mFrameTimeNanos += FRAME_LEN_NANOS;
}
@Before
public void setUp() {
PowerMockito.mockStatic(Arguments.class);
PowerMockito.when(Arguments.createArray()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return new JavaOnlyArray();
}
});
PowerMockito.when(Arguments.createMap()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return new JavaOnlyMap();
}
});
mFrameTimeNanos = INITIAL_FRAME_TIME_NANOS;
mUIManagerMock = mock(UIManagerModule.class);
mUIImplementationMock = mock(UIImplementation.class);
mEventDispatcherMock = mock(EventDispatcher.class);
PowerMockito.when(mUIManagerMock.getUIImplementation()).thenAnswer(new Answer<UIImplementation>() {
@Override
public UIImplementation answer(InvocationOnMock invocation) throws Throwable {
return mUIImplementationMock;
}
});
PowerMockito.when(mUIManagerMock.getEventDispatcher()).thenAnswer(new Answer<EventDispatcher>() {
@Override
public EventDispatcher answer(InvocationOnMock invocation) throws Throwable {
return mEventDispatcherMock;
}
});
PowerMockito.when(mUIManagerMock.getConstants()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return MapBuilder.of("customDirectEventTypes", MapBuilder.newHashMap());
}
});
mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock);
}
/**
* Generates a simple animated nodes graph and attaches the props node to a given {@param viewTag}
* Parameter {@param opacity} is used as a initial value for the "opacity" attribute.
*
* Nodes are connected as follows (nodes IDs in parens):
* ValueNode(1) -> StyleNode(2) -> PropNode(3)
*/
private void createSimpleAnimatedViewWithOpacity(int viewTag, double opacity) {
mNativeAnimatedNodesManager.createAnimatedNode(
1,
JavaOnlyMap.of("type", "value", "value", opacity, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2,
JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("opacity", 1)));
mNativeAnimatedNodesManager.createAnimatedNode(
3,
JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 2)));
mNativeAnimatedNodesManager.connectAnimatedNodes(1, 2);
mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3);
mNativeAnimatedNodesManager.connectAnimatedNodeToView(3, viewTag);
}
@Test
public void testFramesAnimation() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0);
for (int i = 0; i < frames.size(); i++) {
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock)
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN))
.isEqualTo(frames.getDouble(i));
}
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIImplementationMock);
}
@Test
public void testNodeValueListenerIfNotListening() {
int nodeId = 1;
createSimpleAnimatedViewWithOpacity(1000, 0d);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
Callback animationCallback = mock(Callback.class);
AnimatedNodeValueListener valueListener = mock(AnimatedNodeValueListener.class);
mNativeAnimatedNodesManager.startListeningToAnimatedNodeValue(nodeId, valueListener);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
nodeId,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(valueListener).onValueUpdate(eq(0d));
mNativeAnimatedNodesManager.stopListeningToAnimatedNodeValue(nodeId);
reset(valueListener);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(valueListener);
}
@Test
public void testNodeValueListenerIfListening() {
int nodeId = 1;
createSimpleAnimatedViewWithOpacity(1000, 0d);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
Callback animationCallback = mock(Callback.class);
AnimatedNodeValueListener valueListener = mock(AnimatedNodeValueListener.class);
mNativeAnimatedNodesManager.startListeningToAnimatedNodeValue(nodeId, valueListener);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
nodeId,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(valueListener).onValueUpdate(eq(0d));
for (int i = 0; i < frames.size(); i++) {
reset(valueListener);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(valueListener).onValueUpdate(eq(frames.getDouble(i)));
}
reset(valueListener);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(valueListener);
}
@Test
public void testSpringAnimation() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of(
"type",
"spring",
"friction",
7d,
"tension",
40.0d,
"initialVelocity",
0d,
"toValue",
1d,
"restSpeedThreshold",
0.001d,
"restDisplacementThreshold",
0.001d,
"overshootClamping",
false),
animationCallback);
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0);
double previousValue = 0d;
boolean wasGreaterThanOne = false;
/* run 3 secs of animation */
for (int i = 0; i < 3 * 60; i++) {
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN);
if (currentValue > 1d) {
wasGreaterThanOne = true;
}
// verify that animation step is relatively small
assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.1d);
previousValue = currentValue;
}
// verify that we've reach the final value at the end of animation
assertThat(previousValue).isEqualTo(1d);
// verify that value has reached some maximum value that is greater than the final value (bounce)
assertThat(wasGreaterThanOne);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIImplementationMock);
}
@Test
public void testDecayAnimation() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of(
"type",
"decay",
"velocity",
0.5d,
"deceleration",
0.998d),
animationCallback);
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double previousValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN);
double previousDiff = Double.POSITIVE_INFINITY;
/* run 3 secs of animation */
for (int i = 0; i < 3 * 60; i++) {
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN);
double currentDiff = currentValue - previousValue;
// verify monotonicity
// greater *or equal* because the animation stops during these 3 seconds
assertThat(currentValue).as("on frame " + i).isGreaterThanOrEqualTo(previousValue);
// verify decay
if (i > 3) {
// i > 3 because that's how long it takes to settle previousDiff
if (i % 3 != 0) {
// i % 3 != 0 because every 3 frames we go a tiny
// bit faster, because frame length is 16.(6)ms
assertThat(currentDiff).as("on frame " + i).isLessThanOrEqualTo(previousDiff);
} else {
assertThat(currentDiff).as("on frame " + i).isGreaterThanOrEqualTo(previousDiff);
}
}
previousValue = currentValue;
previousDiff = currentDiff;
}
// should be done in 3s
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIImplementationMock);
}
@Test
public void testAnimationCallbackFinish() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);
ArgumentCaptor<ReadableMap> callbackResponseCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(animationCallback);
reset(animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(animationCallback).invoke(callbackResponseCaptor.capture());
assertThat(callbackResponseCaptor.getValue().hasKey("finished")).isTrue();
assertThat(callbackResponseCaptor.getValue().getBoolean("finished")).isTrue();
reset(animationCallback);
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, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2,
JavaOnlyMap.of("type", "value", "value", 1000d, "offset", 0d));
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,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
animationCallback);
mNativeAnimatedNodesManager.startAnimatingNode(
2,
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,
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,
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,
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);
}
@Test
public void testMultiplicationNode() {
mNativeAnimatedNodesManager.createAnimatedNode(
1,
JavaOnlyMap.of("type", "value", "value", 1d, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2,
JavaOnlyMap.of("type", "value", "value", 5d, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
3,
JavaOnlyMap.of("type", "multiplication", "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);
Callback animationCallback = mock(Callback.class);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 2d),
animationCallback);
mNativeAnimatedNodesManager.startAnimatingNode(
2,
2,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 10d),
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(5d);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(5d);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(20d);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIImplementationMock);
}
/**
* This test verifies that when {@link NativeAnimatedModule#stopAnimation} is called the animation
* will no longer be updating the nodes it has been previously attached to and that the animation
* callback will be triggered with {@code {finished: false}}
*/
@Test
public void testHandleStoppingAnimation() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1.0d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
404,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);
ArgumentCaptor<ReadableMap> callbackResponseCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(animationCallback);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock, times(2))
.synchronouslyUpdateViewOnUIThread(anyInt(), any(ReactStylesDiffMap.class));
verifyNoMoreInteractions(animationCallback);
reset(animationCallback);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.stopAnimation(404);
verify(animationCallback).invoke(callbackResponseCaptor.capture());
verifyNoMoreInteractions(animationCallback);
verifyNoMoreInteractions(mUIImplementationMock);
assertThat(callbackResponseCaptor.getValue().hasKey("finished")).isTrue();
assertThat(callbackResponseCaptor.getValue().getBoolean("finished")).isFalse();
reset(animationCallback);
reset(mUIImplementationMock);
// Run "update" loop a few more times -> we expect no further updates nor callback calls to be
// triggered
for (int i = 0; i < 5; i++) {
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
}
verifyNoMoreInteractions(mUIImplementationMock);
verifyNoMoreInteractions(animationCallback);
}
@Test
public void testInterpolationNode() {
mNativeAnimatedNodesManager.createAnimatedNode(
1,
JavaOnlyMap.of("type", "value", "value", 10d, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2,
JavaOnlyMap.of(
"type",
"interpolation",
"inputRange",
JavaOnlyArray.of(10d, 20d),
"outputRange",
JavaOnlyArray.of(0d, 1d),
"extrapolateLeft",
"extend",
"extrapolateRight",
"extend"));
mNativeAnimatedNodesManager.createAnimatedNode(
3,
JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("opacity", 2)));
mNativeAnimatedNodesManager.createAnimatedNode(
4,
JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 3)));
mNativeAnimatedNodesManager.connectAnimatedNodes(1, 2);
mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3);
mNativeAnimatedNodesManager.connectAnimatedNodes(3, 4);
mNativeAnimatedNodesManager.connectAnimatedNodeToView(4, 50);
Callback animationCallback = mock(Callback.class);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 20d),
animationCallback);
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0d);
for (int i = 0; i < frames.size(); i++) {
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock)
.synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN))
.isEqualTo(frames.getDouble(i));
}
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIImplementationMock);
}
private Event createScrollEvent(final int tag, final double value) {
return new Event(tag) {
@Override
public String getEventName() {
return "topScroll";
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(tag, "topScroll", JavaOnlyMap.of(
"contentOffset", JavaOnlyMap.of("y", value)));
}
};
}
@Test
public void testNativeAnimatedEventDoUpdate() {
int viewTag = 1000;
createSimpleAnimatedViewWithOpacity(viewTag, 0d);
mNativeAnimatedNodesManager.addAnimatedEventToView(viewTag, "topScroll", JavaOnlyMap.of(
"animatedValueTag", 1,
"nativeEventPath", JavaOnlyArray.of("contentOffset", "y")));
mNativeAnimatedNodesManager.onEventDispatch(createScrollEvent(viewTag, 10));
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(10);
}
@Test
public void testNativeAnimatedEventDoNotUpdate() {
int viewTag = 1000;
createSimpleAnimatedViewWithOpacity(viewTag, 0d);
mNativeAnimatedNodesManager.addAnimatedEventToView(viewTag, "otherEvent", JavaOnlyMap.of(
"animatedValueTag", 1,
"nativeEventPath", JavaOnlyArray.of("contentOffset", "y")));
mNativeAnimatedNodesManager.addAnimatedEventToView(999, "topScroll", JavaOnlyMap.of(
"animatedValueTag", 1,
"nativeEventPath", JavaOnlyArray.of("contentOffset", "y")));
mNativeAnimatedNodesManager.onEventDispatch(createScrollEvent(viewTag, 10));
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0);
}
@Test
public void testNativeAnimatedEventCustomMapping() {
int viewTag = 1000;
PowerMockito.when(mUIManagerMock.getConstants()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return MapBuilder.of("customDirectEventTypes", MapBuilder.of(
"topScroll", MapBuilder.of("registrationName", "onScroll")
));
}
});
mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock);
createSimpleAnimatedViewWithOpacity(viewTag, 0d);
mNativeAnimatedNodesManager.addAnimatedEventToView(viewTag, "onScroll", JavaOnlyMap.of(
"animatedValueTag", 1,
"nativeEventPath", JavaOnlyArray.of("contentOffset", "y")));
mNativeAnimatedNodesManager.onEventDispatch(createScrollEvent(viewTag, 10));
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(10);
}
}