From 4a82dca3b17a2d6400e9b14100b071e822266427 Mon Sep 17 00:00:00 2001 From: osdnk Date: Tue, 9 Apr 2019 10:03:20 -0700 Subject: [PATCH] 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: https://github.com/facebook/react-native/pull/22883 Differential Revision: D14041747 Pulled By: cpojer fbshipit-source-id: 94c68024ceaa259d9bb145bf4b3107af0b15db88 --- .../Animated/src/__tests__/Animated-test.js | 41 ++++++++ Libraries/Animated/src/nodes/AnimatedNode.js | 99 +++++++++++++++++++ Libraries/Animated/src/nodes/AnimatedValue.js | 91 +---------------- .../Animated/src/nodes/AnimatedValueXY.js | 2 +- .../src/nodes/AnimatedWithChildren.js | 12 +++ 5 files changed, 158 insertions(+), 87 deletions(-) diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js index 930d8a020f2bff..eb527fcc2c736a 100644 --- a/Libraries/Animated/src/__tests__/Animated-test.js +++ b/Libraries/Animated/src/__tests__/Animated-test.js @@ -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(); diff --git a/Libraries/Animated/src/nodes/AnimatedNode.js b/Libraries/Animated/src/nodes/AnimatedNode.js index 1d670f18a159c9..f31d5ef354edfd 100644 --- a/Libraries/Animated/src/nodes/AnimatedNode.js +++ b/Libraries/Animated/src/nodes/AnimatedNode.js @@ -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) { @@ -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( diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js index e896f660000ab2..47fca40e9d5bdc 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -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. @@ -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() { @@ -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. @@ -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 @@ -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. @@ -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 { diff --git a/Libraries/Animated/src/nodes/AnimatedValueXY.js b/Libraries/Animated/src/nodes/AnimatedValueXY.js index adc7a526a1eaa8..15575c7e5f944f 100644 --- a/Libraries/Animated/src/nodes/AnimatedValueXY.js +++ b/Libraries/Animated/src/nodes/AnimatedValueXY.js @@ -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; diff --git a/Libraries/Animated/src/nodes/AnimatedWithChildren.js b/Libraries/Animated/src/nodes/AnimatedWithChildren.js index 3940c4ae206082..3a5ca82d8745b8 100644 --- a/Libraries/Animated/src/nodes/AnimatedWithChildren.js +++ b/Libraries/Animated/src/nodes/AnimatedWithChildren.js @@ -31,6 +31,7 @@ class AnimatedWithChildren extends AnimatedNode { ); } } + super.__makeNative(); } __addChild(child: AnimatedNode): void { @@ -69,6 +70,17 @@ class AnimatedWithChildren extends AnimatedNode { __getChildren(): Array { 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;