Skip to content

Commit

Permalink
Improve type-safety of NativeAnimated with singleOpBatching
Browse files Browse the repository at this point in the history
Summary:
Improve the types of `queueOperation` so flow can assert that the parameters match.

Also in the case of `singleOpBatching == false`, nativeOps is just an alias of NativeModule so there's no overhead.

Changelog: [Internal]

Reviewed By: JoshuaGross

Differential Revision: D36482617

fbshipit-source-id: e000d11b04166cd7a069af024b9c2cc226efa701
  • Loading branch information
javache authored and facebook-github-bot committed May 20, 2022
1 parent 9e0d869 commit d7dd178
Showing 1 changed file with 109 additions and 126 deletions.
235 changes: 109 additions & 126 deletions Libraries/Animated/NativeAnimatedHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ let queue: Array<() => void> = [];
// $FlowFixMe
let singleOpQueue: Array<any> = [];

let useSingleOpBatching =
const useSingleOpBatching =
Platform.OS === 'android' &&
!!NativeAnimatedModule?.queueAndExecuteBatchedOperations &&
ReactNativeFeatureFlags.animatedShouldUseSingleOp();
Expand All @@ -54,25 +54,58 @@ const eventListenerAnimationFinishedCallbacks = {};
let globalEventEmitterGetValueListener: ?EventSubscription = null;
let globalEventEmitterAnimationFinishedListener: ?EventSubscription = null;

const nativeOps: ?typeof NativeAnimatedModule = useSingleOpBatching
? ((function () {
const apis = [
'createAnimatedNode', // 1
'updateAnimatedNodeConfig', // 2
'getValue', // 3
'startListeningToAnimatedNodeValue', // 4
'stopListeningToAnimatedNodeValue', // 5
'connectAnimatedNodes', // 6
'disconnectAnimatedNodes', // 7
'startAnimatingNode', // 8
'stopAnimation', // 9
'setAnimatedNodeValue', // 10
'setAnimatedNodeOffset', // 11
'flattenAnimatedNodeOffset', // 12
'extractAnimatedNodeOffset', // 13
'connectAnimatedNodeToView', // 14
'disconnectAnimatedNodeFromView', // 15
'restoreDefaultValues', // 16
'dropAnimatedNode', // 17
'addAnimatedEventToView', // 18
'removeAnimatedEventFromView', // 19
'addListener', // 20
'removeListener', // 21
];
return apis.reduce((acc, functionName, i) => {
// These indices need to be kept in sync with the indices in native (see NativeAnimatedModule in Java, or the equivalent for any other native platform).
acc[functionName] = i + 1;
return acc;
}, {});
})(): $FlowFixMe)
: NativeAnimatedModule;

/**
* Simple wrappers around NativeAnimatedModule to provide flow and autocomplete support for
* the native module methods
* Wrappers around NativeAnimatedModule to provide flow and autocomplete support for
* the native module methods, and automatic queue management on Android
*/
const API = {
getValue: function (
tag: number,
saveValueCallback: (value: number) => void,
): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
const args = [tag];
invariant(nativeOps, 'Native animated module is not available');
if (useSingleOpBatching) {
if (saveValueCallback) {
eventListenerGetValueCallbacks[tag] = saveValueCallback;
}
// $FlowFixMe
API.queueOperation(nativeOps.getValue, tag);
} else {
args.push(saveValueCallback);
API.queueOperation(nativeOps.getValue, tag, saveValueCallback);
}
API.queueOperation(API.operationMap.getValue, args);
},
setWaitingForIdentifier: function (id: string): void {
waitingForQueuedOperations.add(id);
Expand All @@ -93,7 +126,7 @@ const API = {
}
},
disableQueue: function (): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
invariant(nativeOps, 'Native animated module is not available');

if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
const prevTimeout = flushQueueTimeout;
Expand All @@ -114,10 +147,11 @@ const API = {
}
},
flushQueue: function (): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
flushQueueTimeout = null;

if (Platform.OS === 'android') {
NativeAnimatedModule?.startOperationBatch?.();
NativeAnimatedModule.startOperationBatch?.();
}
if (useSingleOpBatching) {
// Set up event listener for callbacks if it's not set up
Expand All @@ -131,9 +165,7 @@ const API = {
// use RCTDeviceEventEmitter. This reduces overhead of sending lots of
// JSI functions across to native code; but also, TM infrastructure currently
// does not support packing a function into native arrays.
NativeAnimatedModule?.queueAndExecuteBatchedOperations?.(
singleOpQueue.filter(x => typeof x !== 'function'),
);
NativeAnimatedModule.queueAndExecuteBatchedOperations?.(singleOpQueue);
singleOpQueue.length = 0;
} else {
for (let q = 0, l = queue.length; q < l; q++) {
Expand All @@ -142,210 +174,161 @@ const API = {
queue.length = 0;
}
if (Platform.OS === 'android') {
NativeAnimatedModule?.finishOperationBatch?.();
NativeAnimatedModule.finishOperationBatch?.();
}
},
queueOperation: (
// $FlowFixMe
fn: (mode: 'immediate' | 'batch', ...args: Array<any>) => number,
// $FlowFixMe
args: Array<any>,
queueOperation: <Args: $ReadOnlyArray<mixed>, Fn: (...Args) => void>(
fn: Fn,
...args: Args
): void => {
if (useSingleOpBatching) {
// Get the command ID from the queued function, and push that ID and any arguments needed to execute the operation
singleOpQueue.push(fn('batch'), ...args);
// $FlowFixMe: surprise, fn is actually a number
singleOpQueue.push(fn, ...args);
return;
}

// If queueing is explicitly on, *or* the queue has not yet
// been flushed, use the queue. This is to prevent operations
// from being executed out of order.
if (queueOperations || queue.length !== 0) {
queue.push(() => {
fn('immediate', ...args);
});
queue.push(() => fn(...args));
} else {
fn('immediate', ...args);
fn(...args);
}
},
createAnimatedNode: function (tag: number, config: AnimatedNodeConfig): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.createAnimatedNode, [tag, config]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.createAnimatedNode, tag, config);
},
updateAnimatedNodeConfig: function (
tag: number,
config: AnimatedNodeConfig,
): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
if (typeof NativeAnimatedModule.updateAnimatedNodeConfig === 'function') {
API.queueOperation(API.operationMap.updateAnimatedNodeConfig, [
tag,
config,
]);
invariant(nativeOps, 'Native animated module is not available');
if (nativeOps.updateAnimatedNodeConfig) {
API.queueOperation(nativeOps.updateAnimatedNodeConfig, tag, config);
}
},
startListeningToAnimatedNodeValue: function (tag: number) {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.startListeningToAnimatedNodeValue, [
tag,
]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.startListeningToAnimatedNodeValue, tag);
},
stopListeningToAnimatedNodeValue: function (tag: number) {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.stopListeningToAnimatedNodeValue, [
tag,
]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.stopListeningToAnimatedNodeValue, tag);
},
connectAnimatedNodes: function (parentTag: number, childTag: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.connectAnimatedNodes, [
parentTag,
childTag,
]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.connectAnimatedNodes, parentTag, childTag);
},
disconnectAnimatedNodes: function (
parentTag: number,
childTag: number,
): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.disconnectAnimatedNodes, [
parentTag,
childTag,
]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.disconnectAnimatedNodes, parentTag, childTag);
},
startAnimatingNode: function (
animationId: number,
nodeTag: number,
config: AnimatingNodeConfig,
endCallback: EndCallback,
): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
const args = [animationId, nodeTag, config];
invariant(nativeOps, 'Native animated module is not available');
if (useSingleOpBatching) {
if (endCallback) {
eventListenerAnimationFinishedCallbacks[animationId] = endCallback;
}
// $FlowFixMe
API.queueOperation(
nativeOps.startAnimatingNode,
animationId,
nodeTag,
config,
);
} else {
args.push(endCallback);
API.queueOperation(
nativeOps.startAnimatingNode,
animationId,
nodeTag,
config,
endCallback,
);
}
API.queueOperation(API.operationMap.startAnimatingNode, args);
},
stopAnimation: function (animationId: number) {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.stopAnimation, [animationId]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.stopAnimation, animationId);
},
setAnimatedNodeValue: function (nodeTag: number, value: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.setAnimatedNodeValue, [nodeTag, value]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.setAnimatedNodeValue, nodeTag, value);
},
setAnimatedNodeOffset: function (nodeTag: number, offset: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.setAnimatedNodeOffset, [
nodeTag,
offset,
]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.setAnimatedNodeOffset, nodeTag, offset);
},
flattenAnimatedNodeOffset: function (nodeTag: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.flattenAnimatedNodeOffset, [nodeTag]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.flattenAnimatedNodeOffset, nodeTag);
},
extractAnimatedNodeOffset: function (nodeTag: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.extractAnimatedNodeOffset, [nodeTag]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.extractAnimatedNodeOffset, nodeTag);
},
connectAnimatedNodeToView: function (nodeTag: number, viewTag: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.connectAnimatedNodeToView, [
nodeTag,
viewTag,
]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.connectAnimatedNodeToView, nodeTag, viewTag);
},
disconnectAnimatedNodeFromView: function (
nodeTag: number,
viewTag: number,
): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.disconnectAnimatedNodeFromView, [
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(
nativeOps.disconnectAnimatedNodeFromView,
nodeTag,
viewTag,
]);
);
},
restoreDefaultValues: function (nodeTag: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
invariant(nativeOps, 'Native animated module is not available');
// Backwards compat with older native runtimes, can be removed later.
if (NativeAnimatedModule.restoreDefaultValues != null) {
API.queueOperation(API.operationMap.restoreDefaultValues, [nodeTag]);
if (nativeOps.restoreDefaultValues != null) {
API.queueOperation(nativeOps.restoreDefaultValues, nodeTag);
}
},
dropAnimatedNode: function (tag: number): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.dropAnimatedNode, [tag]);
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.dropAnimatedNode, tag);
},
addAnimatedEventToView: function (
viewTag: number,
eventName: string,
eventMapping: EventMapping,
) {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.addAnimatedEventToView, [
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(
nativeOps.addAnimatedEventToView,
viewTag,
eventName,
eventMapping,
]);
);
},
removeAnimatedEventFromView(
viewTag: number,
eventName: string,
animatedNodeTag: number,
) {
invariant(NativeAnimatedModule, 'Native animated module is not available');
API.queueOperation(API.operationMap.removeAnimatedEventFromView, [
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(
nativeOps.removeAnimatedEventFromView,
viewTag,
eventName,
animatedNodeTag,
]);
);
},
// $FlowFixMe
operationMap: (function () {
const apis = [
'createAnimatedNode', // 1
'updateAnimatedNodeConfig', // 2
'getValue', // 3
'startListeningToAnimatedNodeValue', // 4
'stopListeningToAnimatedNodeValue', // 5
'connectAnimatedNodes', // 6
'disconnectAnimatedNodes', // 7
'startAnimatingNode', // 8
'stopAnimation', // 9
'setAnimatedNodeValue', // 10
'setAnimatedNodeOffset', // 11
'flattenAnimatedNodeOffset', // 12
'extractAnimatedNodeOffset', // 13
'connectAnimatedNodeToView', // 14
'disconnectAnimatedNodeFromView', // 15
'restoreDefaultValues', // 16
'dropAnimatedNode', // 17
'addAnimatedEventToView', // 18
'removeAnimatedEventFromView', // 19
'addListener', // 20
'removeListener', // 21
];
return apis.reduce((acc, functionName, i) => {
acc[functionName] = function (
mode: 'batch' | 'immediate',
// $FlowFixMe
...args: Array<any>
): number {
if (mode === 'immediate') {
// $FlowFixMe
NativeAnimatedModule?.[functionName](...args);
}
// These indices need to be kept in sync with the indices in native (see NativeAnimatedModule in Java, or the equivalent for any other native platform).
return i + 1;
};
return acc;
}, {});
})(),
};

function setupGlobalEventEmitterListeners() {
Expand Down

0 comments on commit d7dd178

Please sign in to comment.