Skip to content

Commit

Permalink
Add listener for non-value animated node (#22883)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
osdnk authored and grabbou committed May 6, 2019
1 parent db64104 commit 4a82dca
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 87 deletions.
41 changes: 41 additions & 0 deletions Libraries/Animated/src/__tests__/Animated-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,47 @@ describe('Animated tests', () => {
expect(value1.__getValue()).toBe(1492);
});

it('should get updates for derived animated nodes', () => {
const value1 = new Animated.Value(40);
const value2 = new Animated.Value(50);
const value3 = new Animated.Value(0);
const value4 = Animated.add(value3, Animated.multiply(value1, value2));
const callback = jest.fn();
const view = new Animated.__PropsOnlyForTests(
{
style: {
transform: [
{
translateX: value4,
},
],
},
},
callback,
);
const listener = jest.fn();
const id = value4.addListener(listener);
value3.setValue(137);
expect(listener.mock.calls.length).toBe(1);
expect(listener).toBeCalledWith({value: 2137});
value1.setValue(0);
expect(listener.mock.calls.length).toBe(2);
expect(listener).toBeCalledWith({value: 137});
expect(view.__getValue()).toEqual({
style: {
transform: [
{
translateX: 137,
},
],
},
});
value4.removeListener(id);
value1.setValue(40);
expect(listener.mock.calls.length).toBe(2);
expect(value4.__getValue()).toBe(2137);
});

it('should removeAll', () => {
const value1 = new Animated.Value(0);
const listener = jest.fn();
Expand Down
99 changes: 99 additions & 0 deletions Libraries/Animated/src/nodes/AnimatedNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@

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

const NativeAnimatedAPI = NativeAnimatedHelper.API;
const invariant = require('invariant');

type ValueListenerCallback = (state: {value: number}) => mixed;

let _uniqueId = 1;

// Note(vjeux): this would be better as an interface but flow doesn't
// support them yet
class AnimatedNode {
_listeners: {[key: string]: ValueListenerCallback};
__nativeAnimatedValueListener: ?any;
__attach(): void {}
__detach(): void {
if (this.__isNative && this.__nativeTag != null) {
Expand All @@ -36,11 +43,103 @@ class AnimatedNode {
/* Methods and props used by native Animated impl */
__isNative: boolean;
__nativeTag: ?number;

constructor() {
this._listeners = {};
}

__makeNative() {
if (!this.__isNative) {
throw new Error('This node cannot be made a "native" animated node');
}

if (this.hasListeners()) {
this._startListeningToNativeValueUpdates();
}
}

/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to
* synchronously read the value because it might be driven natively.
*
* See http://facebook.github.io/react-native/docs/animatedvalue.html#addlistener
*/
addListener(callback: (value: any) => mixed): string {
const id = String(_uniqueId++);
this._listeners[id] = callback;
if (this.__isNative) {
this._startListeningToNativeValueUpdates();
}
return id;
}

/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removelistener
*/
removeListener(id: string): void {
delete this._listeners[id];
if (this.__isNative && !this.hasListeners()) {
this._stopListeningForNativeValueUpdates();
}
}

/**
* Remove all registered listeners.
*
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removealllisteners
*/
removeAllListeners(): void {
this._listeners = {};
if (this.__isNative) {
this._stopListeningForNativeValueUpdates();
}
}

hasListeners(): boolean {
return !!Object.keys(this._listeners).length;
}

_startListeningToNativeValueUpdates() {
if (this.__nativeAnimatedValueListener) {
return;
}

NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener(
'onAnimatedValueUpdate',
data => {
if (data.tag !== this.__getNativeTag()) {
return;
}
this._onAnimatedValueUpdateReceived(data.value);
},
);
}

_onAnimatedValueUpdateReceived(value: number) {
this.__callListeners(value);
}

__callListeners(value: number): void {
for (const key in this._listeners) {
this._listeners[key]({value});
}
}

_stopListeningForNativeValueUpdates() {
if (!this.__nativeAnimatedValueListener) {
return;
}

this.__nativeAnimatedValueListener.remove();
this.__nativeAnimatedValueListener = null;
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
}

__getNativeTag(): ?number {
NativeAnimatedHelper.assertNativeAnimatedModule();
invariant(
Expand Down
91 changes: 5 additions & 86 deletions Libraries/Animated/src/nodes/AnimatedValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ import type AnimatedTracking from './AnimatedTracking';

const NativeAnimatedAPI = NativeAnimatedHelper.API;

type ValueListenerCallback = (state: {value: number}) => void;

let _uniqueId = 1;

/**
* Animated works by building a directed acyclic graph of dependencies
* transparently when you render your Animated components.
Expand Down Expand Up @@ -77,15 +73,12 @@ class AnimatedValue extends AnimatedWithChildren {
_offset: number;
_animation: ?Animation;
_tracking: ?AnimatedTracking;
_listeners: {[key: string]: ValueListenerCallback};
__nativeAnimatedValueListener: ?any;

constructor(value: number) {
super();
this._startingValue = this._value = value;
this._offset = 0;
this._animation = null;
this._listeners = {};
}

__detach() {
Expand All @@ -97,14 +90,6 @@ class AnimatedValue extends AnimatedWithChildren {
return this._value + this._offset;
}

__makeNative() {
super.__makeNative();

if (Object.keys(this._listeners).length) {
this._startListeningToNativeValueUpdates();
}
}

/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
Expand Down Expand Up @@ -167,74 +152,6 @@ class AnimatedValue extends AnimatedWithChildren {
}
}

/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to
* synchronously read the value because it might be driven natively.
*
* See http://facebook.github.io/react-native/docs/animatedvalue.html#addlistener
*/
addListener(callback: ValueListenerCallback): string {
const id = String(_uniqueId++);
this._listeners[id] = callback;
if (this.__isNative) {
this._startListeningToNativeValueUpdates();
}
return id;
}

/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removelistener
*/
removeListener(id: string): void {
delete this._listeners[id];
if (this.__isNative && Object.keys(this._listeners).length === 0) {
this._stopListeningForNativeValueUpdates();
}
}

/**
* Remove all registered listeners.
*
* See http://facebook.github.io/react-native/docs/animatedvalue.html#removealllisteners
*/
removeAllListeners(): void {
this._listeners = {};
if (this.__isNative) {
this._stopListeningForNativeValueUpdates();
}
}

_startListeningToNativeValueUpdates() {
if (this.__nativeAnimatedValueListener) {
return;
}

NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener(
'onAnimatedValueUpdate',
data => {
if (data.tag !== this.__getNativeTag()) {
return;
}
this._updateValue(data.value, false /* flush */);
},
);
}

_stopListeningForNativeValueUpdates() {
if (!this.__nativeAnimatedValueListener) {
return;
}

this.__nativeAnimatedValueListener.remove();
this.__nativeAnimatedValueListener = null;
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
}

/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
Expand All @@ -259,6 +176,10 @@ class AnimatedValue extends AnimatedWithChildren {
this._value = this._startingValue;
}

_onAnimatedValueUpdateReceived(value: number): void {
this._updateValue(value, false /*flush*/);
}

/**
* Interpolates the value before updating the property, e.g. mapping 0-1 to
* 0-10.
Expand Down Expand Up @@ -321,9 +242,7 @@ class AnimatedValue extends AnimatedWithChildren {
if (flush) {
_flush(this);
}
for (const key in this._listeners) {
this._listeners[key]({value: this.__getValue()});
}
super.__callListeners(this.__getValue());
}

__getNativeConfig(): Object {
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Animated/src/nodes/AnimatedValueXY.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const AnimatedWithChildren = require('./AnimatedWithChildren');

const invariant = require('invariant');

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

let _uniqueId = 1;

Expand Down
12 changes: 12 additions & 0 deletions Libraries/Animated/src/nodes/AnimatedWithChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AnimatedWithChildren extends AnimatedNode {
);
}
}
super.__makeNative();
}

__addChild(child: AnimatedNode): void {
Expand Down Expand Up @@ -69,6 +70,17 @@ class AnimatedWithChildren extends AnimatedNode {
__getChildren(): Array<AnimatedNode> {
return this._children;
}

__callListeners(value: number): void {
super.__callListeners(value);
if (!this.__isNative) {
for (const child of this._children) {
if (child.__getValue) {
child.__callListeners(child.__getValue());
}
}
}
}
}

module.exports = AnimatedWithChildren;

1 comment on commit 4a82dca

@solidfox
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnimatedNode isn’t really mentioned in any documentation so I’d like to clarify the implication of this change. Do I interpret it correctly that this means we can now listen to calculated and interpolated animated values, not just the original source?

Please sign in to comment.