Support for stopping animations that run on UI thread.

Summary:This change extends animated native module API with `stopAnimation` method that is responsible for interrupting actively running animation as a reslut of a JS call. In order for the `stopAnimation` to understand `animationId` argument I also had to add `animationId` to `startAnimation` method. As JS thread runs in parallel to the thread which executes the animation there is a chance that JS may call `stopAnimation` after the animation has finished. Because of that we are not doing any checks on the `animationId` parameter passed to `stopAnimation` in native and if the animation does not exists in the registry we ignore that call.

**Test Plan**
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/7058

Differential Revision: D3211906

fb-gh-sync-id: 3761509651de36a550b00d33e2a631c379d3900f
fbshipit-source-id: 3761509651de36a550b00d33e2a631c379d3900f
This commit is contained in:
Krzysztof Magiera
2016-04-22 00:02:25 -07:00
committed by Facebook Github Bot 8
parent 63adb48dc4
commit cd11738819
7 changed files with 143 additions and 13 deletions
@@ -17,9 +17,10 @@ import com.facebook.react.bridge.Callback;
*/
/*package*/ abstract class AnimationDriver {
boolean mHasFinished = false;
ValueAnimatedNode mAnimatedValue;
Callback mEndCallback;
/*package*/ boolean mHasFinished = false;
/*package*/ ValueAnimatedNode mAnimatedValue;
/*package*/ Callback mEndCallback;
/*package*/ int mId;
/**
* This method gets called in the main animation loop with a frame time passed down from the
@@ -212,6 +212,7 @@ public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
@ReactMethod
public void startAnimatingNode(
final int animationId,
final int animatedNodeTag,
final ReadableMap animationConfig,
final Callback endCallback) {
@@ -219,6 +220,7 @@ public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.startAnimatingNode(
animationId,
animatedNodeTag,
animationConfig,
endCallback);
@@ -226,6 +228,16 @@ public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
});
}
@ReactMethod
public void stopAnimation(final int animationId) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
animatedNodesManager.stopAnimation(animationId);
}
});
}
@ReactMethod
public void connectAnimatedNodes(final int parentNodeTag, final int childNodeTag) {
mOperations.add(new UIThreadOperation() {
@@ -98,6 +98,7 @@ import javax.annotation.Nullable;
}
public void startAnimatingNode(
int animationId,
int animatedNodeTag,
ReadableMap animationConfig,
Callback endCallback) {
@@ -117,11 +118,34 @@ import javax.annotation.Nullable;
} else {
throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type);
}
animation.mId = animationId;
animation.mEndCallback = endCallback;
animation.mAnimatedValue = (ValueAnimatedNode) node;
mActiveAnimations.add(animation);
}
public void stopAnimation(int animationId) {
// in most of the cases there should never be more than a few active animations running at the
// same time. Therefore it does not make much sense to create an animationId -> animation
// object map that would require additional memory just to support the use-case of stopping
// an animation
for (int i = 0; i < mActiveAnimations.size(); i++) {
AnimationDriver animation = mActiveAnimations.get(i);
if (animation.mId == animationId) {
// Invoke animation end callback with {finished: false}
WritableMap endCallbackResponse = Arguments.createMap();
endCallbackResponse.putBoolean("finished", false);
animation.mEndCallback.invoke(endCallbackResponse);
mActiveAnimations.remove(i);
return;
}
}
// Do not throw an error in the case animation could not be found. We only keep "active"
// animations in the registry and there is a chance that Animated.js will enqueue a
// stopAnimation call after the animation has ended or the call will reach native thread only
// when the animation is already over.
}
public void connectAnimatedNodes(int parentNodeTag, int childNodeTag) {
AnimatedNode parentNode = mAnimatedNodes.get(parentNodeTag);
if (parentNode == null) {
@@ -32,9 +32,12 @@ 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.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;
@@ -110,6 +113,7 @@ public class NativeAnimatedNodeTraversalTest {
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);
@@ -143,6 +147,7 @@ public class NativeAnimatedNodeTraversalTest {
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);
@@ -209,11 +214,13 @@ public class NativeAnimatedNodeTraversalTest {
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);
@@ -258,6 +265,7 @@ public class NativeAnimatedNodeTraversalTest {
Callback animationCallback = mock(Callback.class);
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
animationCallback);
@@ -304,6 +312,7 @@ public class NativeAnimatedNodeTraversalTest {
// 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);
@@ -311,6 +320,7 @@ public class NativeAnimatedNodeTraversalTest {
// 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);
@@ -370,11 +380,13 @@ public class NativeAnimatedNodeTraversalTest {
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);
@@ -401,4 +413,53 @@ public class NativeAnimatedNodeTraversalTest {
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);
}
}