@tamagui/react-native-web-lite
Version:
React Native for Web
523 lines (483 loc) • 16.9 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @format
*/
import NativeAnimatedNonTurboModule from './NativeAnimatedModule'
import NativeAnimatedTurboModule from './NativeAnimatedTurboModule'
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'
import Platform from '../Utilities/Platform'
import ReactNativeFeatureFlags from '../ReactNative/ReactNativeFeatureFlags'
import { invariant } from '@tamagui/react-native-web-internals'
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'
// TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%.
const NativeAnimatedModule =
Platform.OS === 'ios' && global.RN$Bridgeless === true
? NativeAnimatedTurboModule
: NativeAnimatedNonTurboModule
let __nativeAnimatedNodeTagCount = 1 /* used for animated nodes */
let __nativeAnimationIdCount = 1 /* used for started animations */
let nativeEventEmitter
let waitingForQueuedOperations = new Set()
let queueOperations = false
let queue = []
let singleOpQueue = []
const useSingleOpBatching = false
Platform.OS === 'android' &&
!!NativeAnimatedModule?.queueAndExecuteBatchedOperations &&
ReactNativeFeatureFlags.animatedShouldUseSingleOp()
let flushQueueTimeout = null
const eventListenerGetValueCallbacks = {}
const eventListenerAnimationFinishedCallbacks = {}
let globalEventEmitterGetValueListener = null
let globalEventEmitterAnimationFinishedListener = null
const nativeOps = 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
}, {})
})()
: NativeAnimatedModule
/**
* 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, saveValueCallback) {
invariant(nativeOps, 'Native animated module is not available')
if (useSingleOpBatching) {
if (saveValueCallback) {
eventListenerGetValueCallbacks[tag] = saveValueCallback
}
API.queueOperation(nativeOps.getValue, tag)
} else {
API.queueOperation(nativeOps.getValue, tag, saveValueCallback)
}
},
setWaitingForIdentifier: function (id) {
waitingForQueuedOperations.add(id)
queueOperations = true
if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush() && flushQueueTimeout) {
clearTimeout(flushQueueTimeout)
}
},
unsetWaitingForIdentifier: function (id) {
waitingForQueuedOperations.delete(id)
if (waitingForQueuedOperations.size === 0) {
queueOperations = false
API.disableQueue()
}
},
disableQueue: function () {
invariant(nativeOps, 'Native animated module is not available')
if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
const prevTimeout = flushQueueTimeout
clearImmediate(prevTimeout)
flushQueueTimeout = setImmediate(API.flushQueue)
} else {
API.flushQueue()
}
},
flushQueue: function () {
/*
invariant(NativeAnimatedModule, 'Native animated module is not available');
flushQueueTimeout = null;
// Early returns before calling any APIs
if (useSingleOpBatching && singleOpQueue.length === 0) {
return;
}
if (!useSingleOpBatching && queue.length === 0) {
return;
}
if (useSingleOpBatching) {
// Set up event listener for callbacks if it's not set up
if (
!globalEventEmitterGetValueListener ||
!globalEventEmitterAnimationFinishedListener
) {
setupGlobalEventEmitterListeners();
}
// Single op batching doesn't use callback functions, instead we
// 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);
singleOpQueue.length = 0;
} else {
Platform.OS === 'android' && NativeAnimatedModule.startOperationBatch?.();
for (let q = 0, l = queue.length; q < l; q++) {
queue[q]();
}
queue.length = 0;
Platform.OS === 'android' &&
NativeAnimatedModule.finishOperationBatch?.();
}
*/
},
queueOperation: (fn, ...args) => {
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, ...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(...args))
} else {
fn(...args)
}
},
createAnimatedNode: function (tag, config) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.createAnimatedNode, tag, config)
},
updateAnimatedNodeConfig: function (tag, config) {
invariant(nativeOps, 'Native animated module is not available')
//if (nativeOps.updateAnimatedNodeConfig) {
// API.queueOperation(nativeOps.updateAnimatedNodeConfig, tag, config);
//}
},
startListeningToAnimatedNodeValue: function (tag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.startListeningToAnimatedNodeValue, tag)
},
stopListeningToAnimatedNodeValue: function (tag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.stopListeningToAnimatedNodeValue, tag)
},
connectAnimatedNodes: function (parentTag, childTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.connectAnimatedNodes, parentTag, childTag)
},
disconnectAnimatedNodes: function (parentTag, childTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.disconnectAnimatedNodes, parentTag, childTag)
},
startAnimatingNode: function (animationId, nodeTag, config, endCallback) {
invariant(nativeOps, 'Native animated module is not available')
if (useSingleOpBatching) {
if (endCallback) {
eventListenerAnimationFinishedCallbacks[animationId] = endCallback
}
API.queueOperation(nativeOps.startAnimatingNode, animationId, nodeTag, config)
} else {
API.queueOperation(
nativeOps.startAnimatingNode,
animationId,
nodeTag,
config,
endCallback
)
}
},
stopAnimation: function (animationId) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.stopAnimation, animationId)
},
setAnimatedNodeValue: function (nodeTag, value) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.setAnimatedNodeValue, nodeTag, value)
},
setAnimatedNodeOffset: function (nodeTag, offset) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.setAnimatedNodeOffset, nodeTag, offset)
},
flattenAnimatedNodeOffset: function (nodeTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.flattenAnimatedNodeOffset, nodeTag)
},
extractAnimatedNodeOffset: function (nodeTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.extractAnimatedNodeOffset, nodeTag)
},
connectAnimatedNodeToView: function (nodeTag, viewTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.connectAnimatedNodeToView, nodeTag, viewTag)
},
disconnectAnimatedNodeFromView: function (nodeTag, viewTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.disconnectAnimatedNodeFromView, nodeTag, viewTag)
},
restoreDefaultValues: function (nodeTag) {
invariant(nativeOps, 'Native animated module is not available')
// Backwards compat with older native runtimes, can be removed later.
if (nativeOps.restoreDefaultValues != null) {
API.queueOperation(nativeOps.restoreDefaultValues, nodeTag)
}
},
dropAnimatedNode: function (tag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.dropAnimatedNode, tag)
},
addAnimatedEventToView: function (viewTag, eventName, eventMapping) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(nativeOps.addAnimatedEventToView, viewTag, eventName, eventMapping)
},
removeAnimatedEventFromView(viewTag, eventName, animatedNodeTag) {
invariant(nativeOps, 'Native animated module is not available')
API.queueOperation(
nativeOps.removeAnimatedEventFromView,
viewTag,
eventName,
animatedNodeTag
)
},
}
function setupGlobalEventEmitterListeners() {
globalEventEmitterGetValueListener = RCTDeviceEventEmitter.addListener(
'onNativeAnimatedModuleGetValue',
function (params) {
const { tag } = params
const callback = eventListenerGetValueCallbacks[tag]
if (!callback) {
return
}
callback(params.value)
delete eventListenerGetValueCallbacks[tag]
}
)
globalEventEmitterAnimationFinishedListener = RCTDeviceEventEmitter.addListener(
'onNativeAnimatedModuleAnimationFinished',
function (params) {
const { animationId } = params
const callback = eventListenerAnimationFinishedCallbacks[animationId]
if (!callback) {
return
}
callback(params)
delete eventListenerAnimationFinishedCallbacks[animationId]
}
)
}
/**
* Styles allowed by the native animated implementation.
*
* In general native animated implementation should support any numeric or color property that
* doesn't need to be updated through the shadow view hierarchy (all non-layout properties).
*/
const SUPPORTED_COLOR_STYLES = {
backgroundColor: true,
borderBottomColor: true,
borderColor: true,
borderEndColor: true,
borderLeftColor: true,
borderRightColor: true,
borderStartColor: true,
borderTopColor: true,
color: true,
tintColor: true,
}
const SUPPORTED_STYLES = {
...SUPPORTED_COLOR_STYLES,
borderBottomEndRadius: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
borderBottomStartRadius: true,
borderRadius: true,
borderTopEndRadius: true,
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderTopStartRadius: true,
elevation: true,
opacity: true,
transform: true,
zIndex: true,
/* ios styles */
shadowOpacity: true,
shadowRadius: true,
/* legacy android transform properties */
scaleX: true,
scaleY: true,
translateX: true,
translateY: true,
}
const SUPPORTED_TRANSFORMS = {
translateX: true,
translateY: true,
scale: true,
scaleX: true,
scaleY: true,
rotate: true,
rotateX: true,
rotateY: true,
rotateZ: true,
perspective: true,
}
const SUPPORTED_INTERPOLATION_PARAMS = {
inputRange: true,
outputRange: true,
extrapolate: true,
extrapolateRight: true,
extrapolateLeft: true,
}
function addWhitelistedStyleProp(prop) {
SUPPORTED_STYLES[prop] = true
}
function addWhitelistedTransformProp(prop) {
SUPPORTED_TRANSFORMS[prop] = true
}
function addWhitelistedInterpolationParam(param) {
SUPPORTED_INTERPOLATION_PARAMS[param] = true
}
function isSupportedColorStyleProp(prop) {
return SUPPORTED_COLOR_STYLES.hasOwnProperty(prop)
}
function isSupportedStyleProp(prop) {
return SUPPORTED_STYLES.hasOwnProperty(prop)
}
function isSupportedTransformProp(prop) {
return SUPPORTED_TRANSFORMS.hasOwnProperty(prop)
}
function isSupportedInterpolationParam(param) {
return SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(param)
}
function validateTransform(configs) {
configs.forEach((config) => {
if (!isSupportedTransformProp(config.property)) {
throw new Error(
`Property '${config.property}' is not supported by native animated module`
)
}
})
}
function validateStyles(styles) {
for (const key in styles) {
if (!isSupportedStyleProp(key)) {
throw new Error(
`Style property '${key}' is not supported by native animated module`
)
}
}
}
function validateInterpolation(config) {
for (const key in config) {
if (!isSupportedInterpolationParam(key)) {
throw new Error(
`Interpolation property '${key}' is not supported by native animated module`
)
}
}
}
function generateNewNodeTag() {
return __nativeAnimatedNodeTagCount++
}
function generateNewAnimationId() {
return __nativeAnimationIdCount++
}
function assertNativeAnimatedModule() {
invariant(NativeAnimatedModule, 'Native animated module is not available')
}
let _warnedMissingNativeAnimated = false
function shouldUseNativeDriver(config) {
if (config.useNativeDriver == null) {
console.warn(
'Animated: `useNativeDriver` was not specified. This is a required ' +
'option and must be explicitly set to `true` or `false`'
)
}
if (config.useNativeDriver === true && !NativeAnimatedModule) {
if (!_warnedMissingNativeAnimated) {
console.warn(
'Animated: `useNativeDriver` is not supported because the native ' +
'animated module is missing. Falling back to JS-based animation. To ' +
'resolve this, add `RCTAnimation` module to this app, or remove ' +
'`useNativeDriver`. ' +
'Make sure to run `bundle exec pod install` first. Read more about autolinking: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md'
)
_warnedMissingNativeAnimated = true
}
return false
}
return config.useNativeDriver || false
}
function transformDataType(value) {
// Change the string type to number type so we can reuse the same logic in
// iOS and Android platform
if (typeof value !== 'string') {
return value
}
if (/deg$/.test(value)) {
const degrees = parseFloat(value) || 0
const radians = (degrees * Math.PI) / 180.0
return radians
} else {
return value
}
}
export {
API,
isSupportedColorStyleProp,
isSupportedStyleProp,
isSupportedTransformProp,
isSupportedInterpolationParam,
addWhitelistedStyleProp,
addWhitelistedTransformProp,
addWhitelistedInterpolationParam,
validateStyles,
validateTransform,
validateInterpolation,
generateNewNodeTag,
generateNewAnimationId,
assertNativeAnimatedModule,
shouldUseNativeDriver,
transformDataType,
}
export default {
API,
isSupportedColorStyleProp,
isSupportedStyleProp,
isSupportedTransformProp,
isSupportedInterpolationParam,
addWhitelistedStyleProp,
addWhitelistedTransformProp,
addWhitelistedInterpolationParam,
validateStyles,
validateTransform,
validateInterpolation,
generateNewNodeTag,
generateNewAnimationId,
assertNativeAnimatedModule,
shouldUseNativeDriver,
transformDataType,
get nativeEventEmitter() {
if (!nativeEventEmitter) {
nativeEventEmitter = new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeAnimatedModule
)
}
return nativeEventEmitter
},
}