Skip to content

Commit

Permalink
Add listener for non-value animated node (facebook#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: facebook#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
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
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
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
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
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;

0 comments on commit 4a82dca

Please sign in to comment.