Files
react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java
T
Janic Duplessis 686ab49107 Don't restore default values when components unmount (#26978)
Summary:
There are some cases where restoring default values on component unmount is not desirable. For example in react-native-screens we want to keep the native view displayed after react has unmounted them. Restoring default values causes an issue there because it will change props controlled my native animated back to their default value instead of keeping whatever value they had been animated to.

Restoring default values is only needed for updates anyway, where removing a prop controlled by native animated need to be reset to its default value since react no longer tracks its value.

This splits restoring default values and disconnecting from views in 2 separate native methods, this way we can restore default values only on component update and not on unmount. This takes care of being backwards compatible for JS running with the older native code.

## Changelog

[General] [Fixed] - NativeAnimated - Don't restore default values when components unmount
Pull Request resolved: https://github.com/facebook/react-native/pull/26978

Test Plan:
- Tested in an app using react-native-screens to make sure native views that are kept after their underlying component has been unmount don't change. Also tested in RNTester animated example.

- Tested that new JS works with old native code

Reviewed By: mmmulani

Differential Revision: D18197735

Pulled By: JoshuaGross

fbshipit-source-id: 20fa0f31a3edf1bc57ccb03df9d1486aba83edc4
2019-11-04 15:40:09 -08:00

1215 lines
48 KiB
Java

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.animated;
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;
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.UIManagerModule;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.util.Map;
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;
/** Tests the animated nodes graph traversal algorithm from {@link NativeAnimatedNodesManager}. */
@PrepareForTest({Arguments.class})
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "androidx.*", "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 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);
mEventDispatcherMock = mock(EventDispatcher.class);
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());
}
});
PowerMockito.when(mUIManagerMock.getDirectEventNamesResolver())
.thenAnswer(
new Answer<UIManagerModule.CustomEventNamesResolver>() {
@Override
public UIManagerModule.CustomEventNamesResolver answer(InvocationOnMock invocation)
throws Throwable {
return new UIManagerModule.CustomEventNamesResolver() {
@Override
public String resolveCustomEventName(String eventName) {
Map<String, Map> directEventTypes =
(Map<String, Map>)
mUIManagerMock.getConstants().get("customDirectEventTypes");
if (directEventTypes != null) {
Map<String, String> customEventType =
(Map<String, String>) directEventTypes.get(eventName);
if (customEventType != null) {
return customEventType.get("registrationName");
}
}
return eventName;
}
};
}
});
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.
*
* <p>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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
for (int i = 0; i < frames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(frames.getDouble(i));
}
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@Test
public void testFramesAnimationLoopsFiveTimes() {
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, "iterations", 5),
animationCallback);
ArgumentCaptor<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
for (int iteration = 0; iteration < 5; iteration++) {
for (int i = 0; i < frames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(frames.getDouble(i));
}
}
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@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);
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);
}
public void performSpringAnimationTestWithConfig(
JavaOnlyMap config, boolean testForCriticallyDamped) {
createSimpleAnimatedViewWithOpacity(1000, 0d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(1, 1, config, animationCallback);
ArgumentCaptor<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(0);
double previousValue = 0d;
boolean wasGreaterThanOne = false;
/* run 3 secs of animation */
for (int i = 0; i < 3 * 60; i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double currentValue = stylesCaptor.getValue().getDouble("opacity");
if (currentValue > 1d) {
wasGreaterThanOne = true;
}
// verify that animation step is relatively small
assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.12d);
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)
if (testForCriticallyDamped) {
assertThat(!wasGreaterThanOne);
} else {
assertThat(wasGreaterThanOne);
}
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@Test
public void testUnderdampedSpringAnimation() {
performSpringAnimationTestWithConfig(
JavaOnlyMap.of(
"type",
"spring",
"stiffness",
230.2d,
"damping",
22d,
"mass",
1d,
"initialVelocity",
0d,
"toValue",
1d,
"restSpeedThreshold",
0.001d,
"restDisplacementThreshold",
0.001d,
"overshootClamping",
false),
false);
}
@Test
public void testCriticallyDampedSpringAnimation() {
performSpringAnimationTestWithConfig(
JavaOnlyMap.of(
"type",
"spring",
"stiffness",
1000d,
"damping",
500d,
"mass",
3.0d,
"initialVelocity",
0d,
"toValue",
1d,
"restSpeedThreshold",
0.001d,
"restDisplacementThreshold",
0.001d,
"overshootClamping",
false),
true);
}
@Test
public void testSpringAnimationLoopsFiveTimes() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of(
"type",
"spring",
"stiffness",
230.2d,
"damping",
22d,
"mass",
1d,
"initialVelocity",
0d,
"toValue",
1d,
"restSpeedThreshold",
0.001d,
"restDisplacementThreshold",
0.001d,
"overshootClamping",
false,
"iterations",
5),
animationCallback);
ArgumentCaptor<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(0);
double previousValue = 0d;
boolean wasGreaterThanOne = false;
boolean didComeToRest = false;
int numberOfResets = 0;
/* run 3 secs of animation, five times */
for (int i = 0; i < 3 * 60 * 5; i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double currentValue = stylesCaptor.getValue().getDouble("opacity");
if (currentValue > 1d) {
wasGreaterThanOne = true;
}
// Test to see if it reset after coming to rest
if (didComeToRest
&& currentValue == 0d
&& Math.abs(Math.abs(currentValue - previousValue) - 1d) < 0.001d) {
numberOfResets++;
}
// verify that an animation step is relatively small, unless it has come to rest and reset
if (!didComeToRest) assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.12d);
// record that the animation did come to rest when it rests on toValue
didComeToRest =
Math.abs(currentValue - 1d) < 0.001d && Math.abs(currentValue - previousValue) < 0.001d;
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);
// verify that value reset 4 times after finishing a full animation
assertThat(numberOfResets).isEqualTo(4);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double previousValue = stylesCaptor.getValue().getDouble("opacity");
double previousDiff = Double.POSITIVE_INFINITY;
/* run 3 secs of animation */
for (int i = 0; i < 3 * 60; i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double currentValue = stylesCaptor.getValue().getDouble("opacity");
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(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@Test
public void testDecayAnimationLoopsFiveTimes() {
createSimpleAnimatedViewWithOpacity(1000, 0d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "decay", "velocity", 0.5d, "deceleration", 0.998d, "iterations", 5),
animationCallback);
ArgumentCaptor<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double previousValue = stylesCaptor.getValue().getDouble("opacity");
double previousDiff = Double.POSITIVE_INFINITY;
double initialValue = stylesCaptor.getValue().getDouble("opacity");
boolean didComeToRest = false;
int numberOfResets = 0;
/* run 3 secs of animation, five times */
for (int i = 0; i < 3 * 60 * 5; i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, atMost(1))
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
double currentValue = stylesCaptor.getValue().getDouble("opacity");
double currentDiff = currentValue - previousValue;
// Test to see if it reset after coming to rest (i.e. dropped back to )
if (didComeToRest && currentValue == initialValue) {
numberOfResets++;
}
// verify monotonicity, unless it has come to rest and reset
// greater *or equal* because the animation stops during these 3 seconds
if (!didComeToRest)
assertThat(currentValue).as("on frame " + i).isGreaterThanOrEqualTo(previousValue);
// Test if animation has come to rest using the 0.1 threshold from DecayAnimation.java
didComeToRest = Math.abs(currentDiff) < 0.1d;
previousValue = currentValue;
previousDiff = currentDiff;
}
// verify that value reset (looped) 4 times after finishing a full animation
assertThat(numberOfResets).isEqualTo(4);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@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());
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) --+
*
* <p>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", firstValue, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2, JavaOnlyMap.of("type", "value", "value", secondValue, "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, viewTag);
}
@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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(1100d);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(1111d);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
/**
* 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.
*
* <p>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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(1100d);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(1101d);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
/**
* Verifies that {@link NativeAnimatedNodesManager#runUpdates} updates the view correctly in case
* when one of the addition input nodes animation finishes before the other.
*
* <p>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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(1100d);
for (int i = 1; i < secondFrames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX"))
.isEqualTo(1200d + secondFrames.getDouble(i) * 10d);
}
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
@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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(5d);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(20d);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
/**
* 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(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock, times(2))
.synchronouslyUpdateViewOnUIThread(anyInt(), any(ReadableMap.class));
verifyNoMoreInteractions(animationCallback);
reset(animationCallback);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.stopAnimation(404);
verify(animationCallback).invoke(callbackResponseCaptor.capture());
verifyNoMoreInteractions(animationCallback);
verifyNoMoreInteractions(mUIManagerMock);
assertThat(callbackResponseCaptor.getValue().hasKey("finished")).isTrue();
assertThat(callbackResponseCaptor.getValue().getBoolean("finished")).isFalse();
reset(animationCallback);
reset(mUIManagerMock);
// 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(mUIManagerMock);
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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
for (int i = 0; i < frames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(frames.getDouble(i));
}
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).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<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(10);
}
@Test
public void testRestoreDefaultProps() {
int viewTag = 1000;
int propsNodeTag = 3;
mNativeAnimatedNodesManager.createAnimatedNode(
1, JavaOnlyMap.of("type", "value", "value", 1d, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2, JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("opacity", 1)));
mNativeAnimatedNodesManager.createAnimatedNode(
propsNodeTag, JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 2)));
mNativeAnimatedNodesManager.connectAnimatedNodes(1, 2);
mNativeAnimatedNodesManager.connectAnimatedNodes(2, propsNodeTag);
mNativeAnimatedNodesManager.connectAnimatedNodeToView(propsNodeTag, viewTag);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.5d, 1d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1, 1, JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 0d), animationCallback);
ArgumentCaptor<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
for (int i = 0; i < frames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
}
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity")).isEqualTo(0);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.restoreDefaultValues(propsNodeTag);
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().isNull("opacity"));
}
/**
* Creates a following graph of nodes: Value(3, initialValue) ----> Style(4) ---> Props(5) --->
* View(viewTag)
*
* <p>Value(3) is set to track Value(1) via Tracking(2) node with the provided animation config
*/
private void createAnimatedGraphWithTrackingNode(
int viewTag, double initialValue, JavaOnlyMap animationConfig) {
mNativeAnimatedNodesManager.createAnimatedNode(
1, JavaOnlyMap.of("type", "value", "value", initialValue, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
3, JavaOnlyMap.of("type", "value", "value", initialValue, "offset", 0d));
mNativeAnimatedNodesManager.createAnimatedNode(
2,
JavaOnlyMap.of(
"type",
"tracking",
"animationId",
70,
"value",
3,
"toValue",
1,
"animationConfig",
animationConfig));
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, 2);
mNativeAnimatedNodesManager.connectAnimatedNodes(3, 4);
mNativeAnimatedNodesManager.connectAnimatedNodes(4, 5);
mNativeAnimatedNodesManager.connectAnimatedNodeToView(5, viewTag);
}
/**
* In this test we verify that when value is being tracked we can update destination value in the
* middle of ongoing animation and the animation will update and animate to the new spot. This is
* tested using simple 5 frame backed timing animation.
*/
@Test
public void testTracking() {
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.25d, 0.5d, 0.75d, 1d);
JavaOnlyMap animationConfig = JavaOnlyMap.of("type", "frames", "frames", frames);
createAnimatedGraphWithTrackingNode(1000, 0d, animationConfig);
ArgumentCaptor<ReadableMap> stylesCaptor = ArgumentCaptor.forClass(ReadableMap.class);
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX")).isEqualTo(0d);
// update "toValue" to 100, we expect tracking animation to animate now from 0 to 100 in 5 steps
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 100d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // kick off the animation
for (int i = 0; i < frames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX"))
.isEqualTo(frames.getDouble(i) * 100d);
}
// update "toValue" to 0 but run only two frames from the animation,
// we expect tracking animation to animate now from 100 to 75
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 0d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // kick off the animation
for (int i = 0; i < 2; i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX"))
.isEqualTo(100d * (1d - frames.getDouble(i)));
}
// at this point we expect tracking value to be at 75
assertThat(((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue())
.isEqualTo(75d);
// we update "toValue" again to 100 and expect the animation to restart from the current place
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 100d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // kick off the animation
for (int i = 0; i < frames.size(); i++) {
reset(mUIManagerMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIManagerMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("translateX"))
.isEqualTo(50d + 50d * frames.getDouble(i));
}
}
/**
* In this test we verify that when tracking is set up for a given animated node and when the
* animation settles it will not be registered as an active animation and therefore will not
* consume resources on running the animation that has already completed. Then we verify that when
* the value updates the animation will resume as expected and the complete again when reaches the
* end.
*/
@Test
public void testTrackingPausesWhenEndValueIsReached() {
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.5d, 1d);
JavaOnlyMap animationConfig = JavaOnlyMap.of("type", "frames", "frames", frames);
createAnimatedGraphWithTrackingNode(1000, 0d, animationConfig);
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 100d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // make sure animation starts
reset(mUIManagerMock);
for (int i = 0; i < frames.size(); i++) {
assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isTrue();
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
}
verify(mUIManagerMock, times(frames.size()))
.synchronouslyUpdateViewOnUIThread(eq(1000), any(ReadableMap.class));
// the animation has completed, we expect no updates to be done
reset(mUIManagerMock);
assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isFalse();
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
// we update end value and expect the animation to restart
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 200d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // make sure animation starts
reset(mUIManagerMock);
for (int i = 0; i < frames.size(); i++) {
assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isTrue();
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
}
verify(mUIManagerMock, times(frames.size()))
.synchronouslyUpdateViewOnUIThread(eq(1000), any(ReadableMap.class));
// the animation has completed, we expect no updates to be done
reset(mUIManagerMock);
assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isFalse();
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIManagerMock);
}
/**
* In this test we verify that when tracking is configured to use spring animation and when the
* destination value updates the current speed of the animated value will be taken into account
* while updating the spring animation and it will smoothly transition to the new end value.
*/
@Test
public void testSpringTrackingRetainsSpeed() {
// this spring config corresponds to tension 20 and friction 0.5 which makes the spring settle
// very slowly
JavaOnlyMap springConfig =
JavaOnlyMap.of(
"type",
"spring",
"restSpeedThreshold",
0.001,
"mass",
1d,
"restDisplacementThreshold",
0.001,
"initialVelocity",
0.5d,
"damping",
2.5,
"stiffness",
157.8,
"overshootClamping",
false);
createAnimatedGraphWithTrackingNode(1000, 0d, springConfig);
// update "toValue" to 1, we expect tracking animation to animate now from 0 to 1
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 1d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
// we run several steps of animation until the value starts bouncing, has negative speed and
// passes the final point (that is 1) while going backwards
boolean isBoucingBack = false;
double previousValue =
((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue();
for (int maxFrames = 500; maxFrames > 0; maxFrames--) {
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
double currentValue =
((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue();
if (previousValue >= 1d && currentValue < 1d) {
isBoucingBack = true;
break;
}
previousValue = currentValue;
}
assertThat(isBoucingBack).isTrue();
// we now update "toValue" to 1.5 but since the value have negative speed and has also pretty
// low friction we expect it to keep going in the opposite direction for a few more frames
mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 1.5d);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
int bounceBackInitialFrames = 0;
boolean hasTurnedForward = false;
// we run 8 seconds of animation
for (int i = 0; i < 8 * 60; i++) {
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
double currentValue =
((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue();
if (!hasTurnedForward) {
if (currentValue <= previousValue) {
bounceBackInitialFrames++;
} else {
hasTurnedForward = true;
}
}
previousValue = currentValue;
}
assertThat(hasTurnedForward).isEqualTo(true);
assertThat(bounceBackInitialFrames).isGreaterThan(3);
// we verify that the value settled at 2
assertThat(previousValue).isEqualTo(1.5d);
}
}