Skip to content

Commit cd11738

Browse files
kmagieraFacebook Github Bot 8
authored andcommitted
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 facebook#7058 Differential Revision: D3211906 fb-gh-sync-id: 3761509651de36a550b00d33e2a631c379d3900f fbshipit-source-id: 3761509651de36a550b00d33e2a631c379d3900f
1 parent 63adb48 commit cd11738

File tree

7 files changed

+143
-13
lines changed

7 files changed

+143
-13
lines changed

Libraries/Animated/src/AnimatedImplementation.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ type AnimationConfig = {
8282
class Animation {
8383
__active: bool;
8484
__isInteraction: bool;
85-
__nativeTag: number;
85+
__nativeId: number;
8686
__onEnd: ?EndCallback;
8787
start(
8888
fromValue: number,
@@ -91,7 +91,11 @@ class Animation {
9191
previousAnimation: ?Animation,
9292
animatedValue: AnimatedValue
9393
): void {}
94-
stop(): void {}
94+
stop(): void {
95+
if (this.__nativeId) {
96+
NativeAnimatedAPI.stopAnimation(this.__nativeId);
97+
}
98+
}
9599
_getNativeAnimationConfig(): any {
96100
// Subclasses that have corresponding animation implementation done in native
97101
// should override this method
@@ -105,9 +109,9 @@ class Animation {
105109
}
106110
__startNativeAnimation(animatedValue: AnimatedValue): void {
107111
animatedValue.__makeNative();
108-
this.__nativeTag = NativeAnimatedHelper.generateNewAnimationTag();
112+
this.__nativeId = NativeAnimatedHelper.generateNewAnimationId();
109113
NativeAnimatedAPI.startAnimatingNode(
110-
this.__nativeTag,
114+
this.__nativeId,
111115
animatedValue.__getNativeTag(),
112116
this._getNativeAnimationConfig(),
113117
this.__debouncedOnEnd.bind(this)
@@ -311,6 +315,7 @@ class TimingAnimation extends Animation {
311315
}
312316

313317
stop(): void {
318+
super.stop();
314319
this.__active = false;
315320
clearTimeout(this._timeout);
316321
window.cancelAnimationFrame(this._animationFrame);
@@ -381,6 +386,7 @@ class DecayAnimation extends Animation {
381386
}
382387

383388
stop(): void {
389+
super.stop();
384390
this.__active = false;
385391
window.cancelAnimationFrame(this._animationFrame);
386392
this.__debouncedOnEnd({finished: false});
@@ -595,6 +601,7 @@ class SpringAnimation extends Animation {
595601
}
596602

597603
stop(): void {
604+
super.stop();
598605
this.__active = false;
599606
window.cancelAnimationFrame(this._animationFrame);
600607
this.__debouncedOnEnd({finished: false});

Libraries/Animated/src/NativeAnimatedHelper.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var NativeAnimatedModule = require('NativeModules').NativeAnimatedModule;
1616
var invariant = require('fbjs/lib/invariant');
1717

1818
var __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */
19-
var __nativeAnimationTagCount = 1; /* used for started animations */
19+
var __nativeAnimationIdCount = 1; /* used for started animations */
2020

2121
type EndResult = {finished: bool};
2222
type EndCallback = (result: EndResult) => void;
@@ -38,9 +38,13 @@ var API = {
3838
assertNativeAnimatedModule();
3939
NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag);
4040
},
41-
startAnimatingNode: function(animationTag: number, nodeTag: number, config: Object, endCallback: EndCallback): void {
41+
startAnimatingNode: function(animationId: number, nodeTag: number, config: Object, endCallback: EndCallback): void {
4242
assertNativeAnimatedModule();
43-
NativeAnimatedModule.startAnimatingNode(nodeTag, config, endCallback);
43+
NativeAnimatedModule.startAnimatingNode(animationId, nodeTag, config, endCallback);
44+
},
45+
stopAnimation: function(animationId: number) {
46+
assertNativeAnimatedModule();
47+
NativeAnimatedModule.stopAnimation(animationId);
4448
},
4549
setAnimatedNodeValue: function(nodeTag: number, value: number): void {
4650
assertNativeAnimatedModule();
@@ -101,8 +105,8 @@ function generateNewNodeTag(): number {
101105
return __nativeAnimatedNodeTagCount++;
102106
}
103107

104-
function generateNewAnimationTag(): number {
105-
return __nativeAnimationTagCount++;
108+
function generateNewAnimationId(): number {
109+
return __nativeAnimationIdCount++;
106110
}
107111

108112
function assertNativeAnimatedModule(): void {
@@ -114,6 +118,6 @@ module.exports = {
114118
validateProps,
115119
validateStyles,
116120
generateNewNodeTag,
117-
generateNewAnimationTag,
121+
generateNewAnimationId,
118122
assertNativeAnimatedModule,
119123
};

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('Animated', () => {
2828
nativeAnimatedModule.connectAnimatedNodes = jest.genMockFunction();
2929
nativeAnimatedModule.disconnectAnimatedNodes = jest.genMockFunction();
3030
nativeAnimatedModule.startAnimatingNode = jest.genMockFunction();
31+
nativeAnimatedModule.stopAnimation = jest.genMockFunction();
3132
nativeAnimatedModule.setAnimatedNodeValue = jest.genMockFunction();
3233
nativeAnimatedModule.connectAnimatedNodeToView = jest.genMockFunction();
3334
nativeAnimatedModule.disconnectAnimatedNodeFromView = jest.genMockFunction();
@@ -59,6 +60,7 @@ describe('Animated', () => {
5960
expect(nativeAnimatedModule.connectAnimatedNodes.mock.calls.length).toBe(2);
6061

6162
expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith(
63+
jasmine.any(Number),
6264
jasmine.any(Number),
6365
{type: 'frames', frames: jasmine.any(Array), toValue: jasmine.any(Number)},
6466
jasmine.any(Function)
@@ -164,6 +166,7 @@ describe('Animated', () => {
164166

165167
var nativeAnimatedModule = require('NativeModules').NativeAnimatedModule;
166168
expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith(
169+
jasmine.any(Number),
167170
jasmine.any(Number),
168171
{type: 'frames', frames: jasmine.any(Array), toValue: jasmine.any(Number)},
169172
jasmine.any(Function)
@@ -269,4 +272,22 @@ describe('Animated', () => {
269272
.toBeCalledWith(jasmine.any(Number), { type: 'props', props: { style: jasmine.any(Number) }});
270273
});
271274

275+
it('send stopAnimation command to native', () => {
276+
var value = new Animated.Value(0);
277+
var animation = Animated.timing(value, {toValue: 10, duration: 50, useNativeDriver: true});
278+
var nativeAnimatedModule = require('NativeModules').NativeAnimatedModule;
279+
280+
animation.start();
281+
expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith(
282+
jasmine.any(Number),
283+
jasmine.any(Number),
284+
{type: 'frames', frames: jasmine.any(Array), toValue: jasmine.any(Number)},
285+
jasmine.any(Function)
286+
);
287+
var animationId = nativeAnimatedModule.startAnimatingNode.mock.calls[0][0];
288+
289+
animation.stop();
290+
expect(nativeAnimatedModule.stopAnimation).toBeCalledWith(animationId);
291+
});
292+
272293
});

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
*/
1818
/*package*/ abstract class AnimationDriver {
1919

20-
boolean mHasFinished = false;
21-
ValueAnimatedNode mAnimatedValue;
22-
Callback mEndCallback;
20+
/*package*/ boolean mHasFinished = false;
21+
/*package*/ ValueAnimatedNode mAnimatedValue;
22+
/*package*/ Callback mEndCallback;
23+
/*package*/ int mId;
2324

2425
/**
2526
* This method gets called in the main animation loop with a frame time passed down from the

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,20 +212,32 @@ public void execute(NativeAnimatedNodesManager animatedNodesManager) {
212212

213213
@ReactMethod
214214
public void startAnimatingNode(
215+
final int animationId,
215216
final int animatedNodeTag,
216217
final ReadableMap animationConfig,
217218
final Callback endCallback) {
218219
mOperations.add(new UIThreadOperation() {
219220
@Override
220221
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
221222
animatedNodesManager.startAnimatingNode(
223+
animationId,
222224
animatedNodeTag,
223225
animationConfig,
224226
endCallback);
225227
}
226228
});
227229
}
228230

231+
@ReactMethod
232+
public void stopAnimation(final int animationId) {
233+
mOperations.add(new UIThreadOperation() {
234+
@Override
235+
public void execute(NativeAnimatedNodesManager animatedNodesManager) {
236+
animatedNodesManager.stopAnimation(animationId);
237+
}
238+
});
239+
}
240+
229241
@ReactMethod
230242
public void connectAnimatedNodes(final int parentNodeTag, final int childNodeTag) {
231243
mOperations.add(new UIThreadOperation() {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public void setAnimatedNodeValue(int tag, double value) {
9898
}
9999

100100
public void startAnimatingNode(
101+
int animationId,
101102
int animatedNodeTag,
102103
ReadableMap animationConfig,
103104
Callback endCallback) {
@@ -117,11 +118,34 @@ public void startAnimatingNode(
117118
} else {
118119
throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type);
119120
}
121+
animation.mId = animationId;
120122
animation.mEndCallback = endCallback;
121123
animation.mAnimatedValue = (ValueAnimatedNode) node;
122124
mActiveAnimations.add(animation);
123125
}
124126

127+
public void stopAnimation(int animationId) {
128+
// in most of the cases there should never be more than a few active animations running at the
129+
// same time. Therefore it does not make much sense to create an animationId -> animation
130+
// object map that would require additional memory just to support the use-case of stopping
131+
// an animation
132+
for (int i = 0; i < mActiveAnimations.size(); i++) {
133+
AnimationDriver animation = mActiveAnimations.get(i);
134+
if (animation.mId == animationId) {
135+
// Invoke animation end callback with {finished: false}
136+
WritableMap endCallbackResponse = Arguments.createMap();
137+
endCallbackResponse.putBoolean("finished", false);
138+
animation.mEndCallback.invoke(endCallbackResponse);
139+
mActiveAnimations.remove(i);
140+
return;
141+
}
142+
}
143+
// Do not throw an error in the case animation could not be found. We only keep "active"
144+
// animations in the registry and there is a chance that Animated.js will enqueue a
145+
// stopAnimation call after the animation has ended or the call will reach native thread only
146+
// when the animation is already over.
147+
}
148+
125149
public void connectAnimatedNodes(int parentNodeTag, int childNodeTag) {
126150
AnimatedNode parentNode = mAnimatedNodes.get(parentNodeTag);
127151
if (parentNode == null) {

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@
3232
import org.robolectric.RobolectricTestRunner;
3333

3434
import static org.fest.assertions.api.Assertions.assertThat;
35+
import static org.mockito.Matchers.any;
36+
import static org.mockito.Matchers.anyInt;
3537
import static org.mockito.Matchers.eq;
3638
import static org.mockito.Mockito.mock;
3739
import static org.mockito.Mockito.reset;
40+
import static org.mockito.Mockito.times;
3841
import static org.mockito.Mockito.verify;
3942
import static org.mockito.Mockito.verifyNoMoreInteractions;
4043

@@ -110,6 +113,7 @@ public void testFramesAnimation() {
110113
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
111114
Callback animationCallback = mock(Callback.class);
112115
mNativeAnimatedNodesManager.startAnimatingNode(
116+
1,
113117
1,
114118
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
115119
animationCallback);
@@ -143,6 +147,7 @@ public void testAnimationCallbackFinish() {
143147
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
144148
Callback animationCallback = mock(Callback.class);
145149
mNativeAnimatedNodesManager.startAnimatingNode(
150+
1,
146151
1,
147152
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
148153
animationCallback);
@@ -209,11 +214,13 @@ public void testAdditionNode() {
209214
Callback animationCallback = mock(Callback.class);
210215
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
211216
mNativeAnimatedNodesManager.startAnimatingNode(
217+
1,
212218
1,
213219
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
214220
animationCallback);
215221

216222
mNativeAnimatedNodesManager.startAnimatingNode(
223+
2,
217224
2,
218225
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1010d),
219226
animationCallback);
@@ -258,6 +265,7 @@ public void testViewReceiveUpdatesIfOneOfAnimationHasntStarted() {
258265
Callback animationCallback = mock(Callback.class);
259266
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
260267
mNativeAnimatedNodesManager.startAnimatingNode(
268+
1,
261269
1,
262270
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d),
263271
animationCallback);
@@ -304,13 +312,15 @@ public void testViewReceiveUpdatesWhenOneOfAnimationHasFinished() {
304312
// Start animating for the first addition input node, will have 2 frames only
305313
JavaOnlyArray firstFrames = JavaOnlyArray.of(0d, 1d);
306314
mNativeAnimatedNodesManager.startAnimatingNode(
315+
1,
307316
1,
308317
JavaOnlyMap.of("type", "frames", "frames", firstFrames, "toValue", 200d),
309318
animationCallback);
310319

311320
// Start animating for the first addition input node, will have 6 frames
312321
JavaOnlyArray secondFrames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
313322
mNativeAnimatedNodesManager.startAnimatingNode(
323+
2,
314324
2,
315325
JavaOnlyMap.of("type", "frames", "frames", secondFrames, "toValue", 1010d),
316326
animationCallback);
@@ -370,11 +380,13 @@ public void testMultiplicationNode() {
370380
Callback animationCallback = mock(Callback.class);
371381
JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
372382
mNativeAnimatedNodesManager.startAnimatingNode(
383+
1,
373384
1,
374385
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 2d),
375386
animationCallback);
376387

377388
mNativeAnimatedNodesManager.startAnimatingNode(
389+
2,
378390
2,
379391
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 10d),
380392
animationCallback);
@@ -401,4 +413,53 @@ public void testMultiplicationNode() {
401413
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
402414
verifyNoMoreInteractions(mUIImplementationMock);
403415
}
416+
417+
/**
418+
* This test verifies that when {@link NativeAnimatedModule#stopAnimation} is called the animation
419+
* will no longer be updating the nodes it has been previously attached to and that the animation
420+
* callback will be triggered with {@code {finished: false}}
421+
*/
422+
@Test
423+
public void testHandleStoppingAnimation() {
424+
createSimpleAnimatedViewWithOpacity(1000, 0d);
425+
426+
JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1.0d);
427+
Callback animationCallback = mock(Callback.class);
428+
mNativeAnimatedNodesManager.startAnimatingNode(
429+
404,
430+
1,
431+
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
432+
animationCallback);
433+
434+
ArgumentCaptor<ReadableMap> callbackResponseCaptor = ArgumentCaptor.forClass(ReadableMap.class);
435+
436+
reset(animationCallback);
437+
reset(mUIImplementationMock);
438+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
439+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
440+
verify(mUIImplementationMock, times(2))
441+
.synchronouslyUpdateViewOnUIThread(anyInt(), any(ReactStylesDiffMap.class));
442+
verifyNoMoreInteractions(animationCallback);
443+
444+
reset(animationCallback);
445+
reset(mUIImplementationMock);
446+
mNativeAnimatedNodesManager.stopAnimation(404);
447+
verify(animationCallback).invoke(callbackResponseCaptor.capture());
448+
verifyNoMoreInteractions(animationCallback);
449+
verifyNoMoreInteractions(mUIImplementationMock);
450+
451+
assertThat(callbackResponseCaptor.getValue().hasKey("finished")).isTrue();
452+
assertThat(callbackResponseCaptor.getValue().getBoolean("finished")).isFalse();
453+
454+
reset(animationCallback);
455+
reset(mUIImplementationMock);
456+
// Run "update" loop a few more times -> we expect no further updates nor callback calls to be
457+
// triggered
458+
for (int i = 0; i < 5; i++) {
459+
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
460+
}
461+
462+
verifyNoMoreInteractions(mUIImplementationMock);
463+
verifyNoMoreInteractions(animationCallback);
464+
}
404465
}

0 commit comments

Comments
 (0)