Skip to content

Commit 68a5cee

Browse files
osdnkfacebook-github-bot
authored andcommitted
Add listener for non-value animated node (#22883)
Summary: Changelog: ---------- [Changed][General] Move callback-related logic to `AnimatedNode` class in order to make it possible to add the listener for other animated nodes than `AnimatedValue`. I observed that native code appears to be fully prepared for listening not only to animated value but animated nodes generally. Therefore I managed to modify js code for exposing `addListener` method from `AnimatedNode` class instead of `AnimatedValue`. It called for some minor changes, which are not breaking. If you're fine with these changes, I could add proper docs if needed. Pull Request resolved: #22883 Differential Revision: D14041747 Pulled By: cpojer fbshipit-source-id: 94c68024ceaa259d9bb145bf4b3107af0b15db88
1 parent 2d8ad07 commit 68a5cee

File tree

5 files changed

+158
-87
lines changed

5 files changed

+158
-87
lines changed

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,47 @@ describe('Animated tests', () => {
834834
expect(value1.__getValue()).toBe(1492);
835835
});
836836

837+
it('should get updates for derived animated nodes', () => {
838+
const value1 = new Animated.Value(40);
839+
const value2 = new Animated.Value(50);
840+
const value3 = new Animated.Value(0);
841+
const value4 = Animated.add(value3, Animated.multiply(value1, value2));
842+
const callback = jest.fn();
843+
const view = new Animated.__PropsOnlyForTests(
844+
{
845+
style: {
846+
transform: [
847+
{
848+
translateX: value4,
849+
},
850+
],
851+
},
852+
},
853+
callback,
854+
);
855+
const listener = jest.fn();
856+
const id = value4.addListener(listener);
857+
value3.setValue(137);
858+
expect(listener.mock.calls.length).toBe(1);
859+
expect(listener).toBeCalledWith({value: 2137});
860+
value1.setValue(0);
861+
expect(listener.mock.calls.length).toBe(2);
862+
expect(listener).toBeCalledWith({value: 137});
863+
expect(view.__getValue()).toEqual({
864+
style: {
865+
transform: [
866+
{
867+
translateX: 137,
868+
},
869+
],
870+
},
871+
});
872+
value4.removeListener(id);
873+
value1.setValue(40);
874+
expect(listener.mock.calls.length).toBe(2);
875+
expect(value4.__getValue()).toBe(2137);
876+
});
877+
837878
it('should removeAll', () => {
838879
const value1 = new Animated.Value(0);
839880
const listener = jest.fn();

Libraries/Animated/src/nodes/AnimatedNode.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@
1111

1212
const NativeAnimatedHelper = require('../NativeAnimatedHelper');
1313

14+
const NativeAnimatedAPI = NativeAnimatedHelper.API;
1415
const invariant = require('invariant');
1516

17+
type ValueListenerCallback = (state: {value: number}) => mixed;
18+
19+
let _uniqueId = 1;
20+
1621
// Note(vjeux): this would be better as an interface but flow doesn't
1722
// support them yet
1823
class AnimatedNode {
24+
_listeners: {[key: string]: ValueListenerCallback};
25+
__nativeAnimatedValueListener: ?any;
1926
__attach(): void {}
2027
__detach(): void {
2128
if (this.__isNative && this.__nativeTag != null) {
@@ -36,11 +43,103 @@ class AnimatedNode {
3643
/* Methods and props used by native Animated impl */
3744
__isNative: boolean;
3845
__nativeTag: ?number;
46+
47+
constructor() {
48+
this._listeners = {};
49+
}
50+
3951
__makeNative() {
4052
if (!this.__isNative) {
4153
throw new Error('This node cannot be made a "native" animated node');
4254
}
55+
56+
if (this.hasListeners()) {
57+
this._startListeningToNativeValueUpdates();
58+
}
4359
}
60+
61+
/**
62+
* Adds an asynchronous listener to the value so you can observe updates from
63+
* animations. This is useful because there is no way to
64+
* synchronously read the value because it might be driven natively.
65+
*
66+
* See http://facebook.github.io/react-native/docs/animatedvalue.html#addlistener
67+
*/
68+
addListener(callback: (value: any) => mixed): string {
69+
const id = String(_uniqueId++);
70+
this._listeners[id] = callback;
71+
if (this.__isNative) {
72+
this._startListeningToNativeValueUpdates();
73+
}
74+
return id;
75+
}
76+
77+
/**
78+
* Unregister a listener. The `id` param shall match the identifier
79+
* previously returned by `addListener()`.
80+
*
81+
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removelistener
82+
*/
83+
removeListener(id: string): void {
84+
delete this._listeners[id];
85+
if (this.__isNative && !this.hasListeners()) {
86+
this._stopListeningForNativeValueUpdates();
87+
}
88+
}
89+
90+
/**
91+
* Remove all registered listeners.
92+
*
93+
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removealllisteners
94+
*/
95+
removeAllListeners(): void {
96+
this._listeners = {};
97+
if (this.__isNative) {
98+
this._stopListeningForNativeValueUpdates();
99+
}
100+
}
101+
102+
hasListeners(): boolean {
103+
return !!Object.keys(this._listeners).length;
104+
}
105+
106+
_startListeningToNativeValueUpdates() {
107+
if (this.__nativeAnimatedValueListener) {
108+
return;
109+
}
110+
111+
NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
112+
this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener(
113+
'onAnimatedValueUpdate',
114+
data => {
115+
if (data.tag !== this.__getNativeTag()) {
116+
return;
117+
}
118+
this._onAnimatedValueUpdateReceived(data.value);
119+
},
120+
);
121+
}
122+
123+
_onAnimatedValueUpdateReceived(value: number) {
124+
this.__callListeners(value);
125+
}
126+
127+
__callListeners(value: number): void {
128+
for (const key in this._listeners) {
129+
this._listeners[key]({value});
130+
}
131+
}
132+
133+
_stopListeningForNativeValueUpdates() {
134+
if (!this.__nativeAnimatedValueListener) {
135+
return;
136+
}
137+
138+
this.__nativeAnimatedValueListener.remove();
139+
this.__nativeAnimatedValueListener = null;
140+
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
141+
}
142+
44143
__getNativeTag(): ?number {
45144
NativeAnimatedHelper.assertNativeAnimatedModule();
46145
invariant(

Libraries/Animated/src/nodes/AnimatedValue.js

Lines changed: 5 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ import type AnimatedTracking from './AnimatedTracking';
2020

2121
const NativeAnimatedAPI = NativeAnimatedHelper.API;
2222

23-
type ValueListenerCallback = (state: {value: number}) => void;
24-
25-
let _uniqueId = 1;
26-
2723
/**
2824
* Animated works by building a directed acyclic graph of dependencies
2925
* transparently when you render your Animated components.
@@ -77,15 +73,12 @@ class AnimatedValue extends AnimatedWithChildren {
7773
_offset: number;
7874
_animation: ?Animation;
7975
_tracking: ?AnimatedTracking;
80-
_listeners: {[key: string]: ValueListenerCallback};
81-
__nativeAnimatedValueListener: ?any;
8276

8377
constructor(value: number) {
8478
super();
8579
this._startingValue = this._value = value;
8680
this._offset = 0;
8781
this._animation = null;
88-
this._listeners = {};
8982
}
9083

9184
__detach() {
@@ -97,14 +90,6 @@ class AnimatedValue extends AnimatedWithChildren {
9790
return this._value + this._offset;
9891
}
9992

100-
__makeNative() {
101-
super.__makeNative();
102-
103-
if (Object.keys(this._listeners).length) {
104-
this._startListeningToNativeValueUpdates();
105-
}
106-
}
107-
10893
/**
10994
* Directly set the value. This will stop any animations running on the value
11095
* and update all the bound properties.
@@ -167,74 +152,6 @@ class AnimatedValue extends AnimatedWithChildren {
167152
}
168153
}
169154

170-
/**
171-
* Adds an asynchronous listener to the value so you can observe updates from
172-
* animations. This is useful because there is no way to
173-
* synchronously read the value because it might be driven natively.
174-
*
175-
* See http://facebook.github.io/react-native/docs/animatedvalue.html#addlistener
176-
*/
177-
addListener(callback: ValueListenerCallback): string {
178-
const id = String(_uniqueId++);
179-
this._listeners[id] = callback;
180-
if (this.__isNative) {
181-
this._startListeningToNativeValueUpdates();
182-
}
183-
return id;
184-
}
185-
186-
/**
187-
* Unregister a listener. The `id` param shall match the identifier
188-
* previously returned by `addListener()`.
189-
*
190-
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removelistener
191-
*/
192-
removeListener(id: string): void {
193-
delete this._listeners[id];
194-
if (this.__isNative && Object.keys(this._listeners).length === 0) {
195-
this._stopListeningForNativeValueUpdates();
196-
}
197-
}
198-
199-
/**
200-
* Remove all registered listeners.
201-
*
202-
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removealllisteners
203-
*/
204-
removeAllListeners(): void {
205-
this._listeners = {};
206-
if (this.__isNative) {
207-
this._stopListeningForNativeValueUpdates();
208-
}
209-
}
210-
211-
_startListeningToNativeValueUpdates() {
212-
if (this.__nativeAnimatedValueListener) {
213-
return;
214-
}
215-
216-
NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
217-
this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener(
218-
'onAnimatedValueUpdate',
219-
data => {
220-
if (data.tag !== this.__getNativeTag()) {
221-
return;
222-
}
223-
this._updateValue(data.value, false /* flush */);
224-
},
225-
);
226-
}
227-
228-
_stopListeningForNativeValueUpdates() {
229-
if (!this.__nativeAnimatedValueListener) {
230-
return;
231-
}
232-
233-
this.__nativeAnimatedValueListener.remove();
234-
this.__nativeAnimatedValueListener = null;
235-
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
236-
}
237-
238155
/**
239156
* Stops any running animation or tracking. `callback` is invoked with the
240157
* final value after stopping the animation, which is useful for updating
@@ -259,6 +176,10 @@ class AnimatedValue extends AnimatedWithChildren {
259176
this._value = this._startingValue;
260177
}
261178

179+
_onAnimatedValueUpdateReceived(value: number): void {
180+
this._updateValue(value, false /*flush*/);
181+
}
182+
262183
/**
263184
* Interpolates the value before updating the property, e.g. mapping 0-1 to
264185
* 0-10.
@@ -321,9 +242,7 @@ class AnimatedValue extends AnimatedWithChildren {
321242
if (flush) {
322243
_flush(this);
323244
}
324-
for (const key in this._listeners) {
325-
this._listeners[key]({value: this.__getValue()});
326-
}
245+
super.__callListeners(this.__getValue());
327246
}
328247

329248
__getNativeConfig(): Object {

Libraries/Animated/src/nodes/AnimatedValueXY.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const AnimatedWithChildren = require('./AnimatedWithChildren');
1414

1515
const invariant = require('invariant');
1616

17-
type ValueXYListenerCallback = (value: {x: number, y: number}) => void;
17+
type ValueXYListenerCallback = (value: {x: number, y: number}) => mixed;
1818

1919
let _uniqueId = 1;
2020

Libraries/Animated/src/nodes/AnimatedWithChildren.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class AnimatedWithChildren extends AnimatedNode {
3131
);
3232
}
3333
}
34+
super.__makeNative();
3435
}
3536

3637
__addChild(child: AnimatedNode): void {
@@ -69,6 +70,17 @@ class AnimatedWithChildren extends AnimatedNode {
6970
__getChildren(): Array<AnimatedNode> {
7071
return this._children;
7172
}
73+
74+
__callListeners(value: number): void {
75+
super.__callListeners(value);
76+
if (!this.__isNative) {
77+
for (const child of this._children) {
78+
if (child.__getValue) {
79+
child.__callListeners(child.__getValue());
80+
}
81+
}
82+
}
83+
}
7284
}
7385

7486
module.exports = AnimatedWithChildren;

0 commit comments

Comments
 (0)