390 lines
13 KiB
JavaScript
390 lines
13 KiB
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.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
*/
|
|
|
|
import type {EventSubscription} from '../EventEmitter/NativeEventEmitter';
|
|
import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps';
|
|
|
|
import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper';
|
|
import {useAnimatedPropsMemo} from '../../src/private/animated/useAnimatedPropsMemo';
|
|
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
|
|
import {isPublicInstance as isFabricPublicInstance} from '../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils';
|
|
import useRefEffect from '../Utilities/useRefEffect';
|
|
import {AnimatedEvent} from './AnimatedEvent';
|
|
import AnimatedNode from './nodes/AnimatedNode';
|
|
import AnimatedProps from './nodes/AnimatedProps';
|
|
import AnimatedValue from './nodes/AnimatedValue';
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useInsertionEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useReducer,
|
|
useRef,
|
|
} from 'react';
|
|
|
|
type ReducedProps<TProps> = {
|
|
...TProps,
|
|
collapsable: boolean,
|
|
...
|
|
};
|
|
type CallbackRef<T> = T => mixed;
|
|
|
|
type UpdateCallback = () => void;
|
|
|
|
type AnimatedValueListeners = Array<{
|
|
propValue: AnimatedValue,
|
|
listenerId: string,
|
|
}>;
|
|
|
|
const useMemoOrAnimatedPropsMemo =
|
|
ReactNativeFeatureFlags.enableAnimatedPropsMemo()
|
|
? useAnimatedPropsMemo
|
|
: useMemo;
|
|
|
|
export default function useAnimatedProps<TProps: {...}, TInstance>(
|
|
props: TProps,
|
|
allowlist?: ?AnimatedPropsAllowlist,
|
|
): [ReducedProps<TProps>, CallbackRef<TInstance | null>] {
|
|
const [, scheduleUpdate] = useReducer<number, void>(count => count + 1, 0);
|
|
const onUpdateRef = useRef<UpdateCallback | null>(null);
|
|
const timerRef = useRef<TimeoutID | null>(null);
|
|
|
|
const allowlistIfEnabled = ReactNativeFeatureFlags.enableAnimatedAllowlist()
|
|
? allowlist
|
|
: null;
|
|
|
|
const node = useMemoOrAnimatedPropsMemo(
|
|
() =>
|
|
new AnimatedProps(
|
|
props,
|
|
() => onUpdateRef.current?.(),
|
|
allowlistIfEnabled,
|
|
),
|
|
[allowlistIfEnabled, props],
|
|
);
|
|
|
|
const useNativePropsInFabric =
|
|
ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric();
|
|
|
|
const useAnimatedPropsLifecycle =
|
|
ReactNativeFeatureFlags.useInsertionEffectsForAnimations()
|
|
? useAnimatedPropsLifecycle_insertionEffects
|
|
: useAnimatedPropsLifecycle_layoutEffects;
|
|
|
|
useAnimatedPropsLifecycle(node);
|
|
|
|
// TODO: This "effect" does three things:
|
|
//
|
|
// 1) Call `setNativeView`.
|
|
// 2) Update `onUpdateRef`.
|
|
// 3) Update listeners for `AnimatedEvent` props.
|
|
//
|
|
// Ideally, each of these would be separate "effects" so that they are not
|
|
// unnecessarily re-run when irrelevant dependencies change. For example, we
|
|
// should be able to hoist all `AnimatedEvent` props and only do #3 if either
|
|
// the `AnimatedEvent` props change or `instance` changes.
|
|
//
|
|
// But there is no way to transparently compose three separate callback refs,
|
|
// so we just combine them all into one for now.
|
|
const refEffect = useCallback(
|
|
(instance: TInstance) => {
|
|
// NOTE: This may be called more often than necessary (e.g. when `props`
|
|
// changes), but `setNativeView` already optimizes for that.
|
|
node.setNativeView(instance);
|
|
|
|
// NOTE: When using the JS animation driver, this callback is called on
|
|
// every animation frame. When using the native driver, this callback is
|
|
// called when the animation completes.
|
|
onUpdateRef.current = () => {
|
|
if (process.env.NODE_ENV === 'test') {
|
|
// Check 1: this is a test.
|
|
// call `scheduleUpdate` to bypass use of setNativeProps.
|
|
return scheduleUpdate();
|
|
}
|
|
|
|
const isFabricNode = isFabricInstance(instance);
|
|
if (node.__isNative) {
|
|
// Check 2: this is an animation driven by native.
|
|
// In native driven animations, this callback is only called once the animation completes.
|
|
if (isFabricNode) {
|
|
// Call `scheduleUpdate` to synchronise Fiber and Shadow tree.
|
|
// Must not be called in Paper.
|
|
scheduleUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (
|
|
typeof instance !== 'object' ||
|
|
typeof instance?.setNativeProps !== 'function'
|
|
) {
|
|
// Check 3: the instance does not support setNativeProps. Call `scheduleUpdate`.
|
|
return scheduleUpdate();
|
|
}
|
|
|
|
if (!isFabricNode) {
|
|
// Check 4: this is a paper instance, call setNativeProps.
|
|
// $FlowIgnore[not-a-function] - Assume it's still a function.
|
|
// $FlowFixMe[incompatible-use]
|
|
return instance.setNativeProps(node.__getAnimatedValue());
|
|
}
|
|
|
|
if (!useNativePropsInFabric) {
|
|
// Check 5: setNativeProps are disabled.
|
|
return scheduleUpdate();
|
|
}
|
|
|
|
// This is a Fabric instance and setNativeProps is supported.
|
|
|
|
// $FlowIgnore[not-a-function] - Assume it's still a function.
|
|
// $FlowFixMe[incompatible-use]
|
|
instance.setNativeProps(node.__getAnimatedValue());
|
|
|
|
// Keeping state of Fiber tree and Shadow tree in sync.
|
|
//
|
|
// This is done by calling `scheduleUpdate` which will trigger a commit.
|
|
// However, React commit is not fast enough to drive animations.
|
|
// This is where setNativeProps comes in handy but the state between
|
|
// Fiber tree and Shadow tree needs to be kept in sync.
|
|
// The goal is to call `scheduleUpdate` as little as possible to maintain
|
|
// performance but frequently enough to keep state in sync.
|
|
// Debounce is set to 48ms, which is 3 * the duration of a frame.
|
|
// 3 frames was the highest value where flickering state was not observed.
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
timerRef.current = setTimeout(() => {
|
|
timerRef.current = null;
|
|
scheduleUpdate();
|
|
}, 48);
|
|
};
|
|
|
|
const target = getEventTarget(instance);
|
|
const events = [];
|
|
const animatedValueListeners: AnimatedValueListeners = [];
|
|
|
|
for (const propName in props) {
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
const propValue = props[propName];
|
|
if (propValue instanceof AnimatedEvent && propValue.__isNative) {
|
|
propValue.__attach(target, propName);
|
|
events.push([propName, propValue]);
|
|
// $FlowFixMe[incompatible-call] - the `addListenersToPropsValue` drills down the propValue.
|
|
addListenersToPropsValue(propValue, animatedValueListeners);
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
onUpdateRef.current = null;
|
|
|
|
for (const [propName, propValue] of events) {
|
|
propValue.__detach(target, propName);
|
|
}
|
|
|
|
for (const {propValue, listenerId} of animatedValueListeners) {
|
|
propValue.removeListener(listenerId);
|
|
}
|
|
};
|
|
},
|
|
[node, useNativePropsInFabric, props],
|
|
);
|
|
const callbackRef = useRefEffect<TInstance>(refEffect);
|
|
|
|
return [reduceAnimatedProps<TProps>(node, props), callbackRef];
|
|
}
|
|
|
|
function reduceAnimatedProps<TProps>(
|
|
node: AnimatedProps,
|
|
props: TProps,
|
|
): ReducedProps<TProps> {
|
|
// Force `collapsable` to be false so that the native view is not flattened.
|
|
// Flattened views cannot be accurately referenced by the native driver.
|
|
return {
|
|
...(ReactNativeFeatureFlags.enableAnimatedPropsMemo()
|
|
? node.__getValueWithStaticProps(props)
|
|
: node.__getValue()),
|
|
collapsable: false,
|
|
};
|
|
}
|
|
|
|
function addListenersToPropsValue(
|
|
propValue: AnimatedValue,
|
|
accumulator: AnimatedValueListeners,
|
|
) {
|
|
// propValue can be a scalar value, an array or an object.
|
|
if (propValue instanceof AnimatedValue) {
|
|
const listenerId = propValue.addListener(() => {});
|
|
accumulator.push({propValue, listenerId});
|
|
} else if (Array.isArray(propValue)) {
|
|
// An array can be an array of scalar values, arrays of arrays, or arrays of objects
|
|
for (const prop of propValue) {
|
|
addListenersToPropsValue(prop, accumulator);
|
|
}
|
|
} else if (propValue instanceof Object) {
|
|
addAnimatedValuesListenersToProps(propValue, accumulator);
|
|
}
|
|
}
|
|
|
|
function addAnimatedValuesListenersToProps(
|
|
props: AnimatedNode,
|
|
accumulator: AnimatedValueListeners,
|
|
) {
|
|
for (const propName in props) {
|
|
// $FlowFixMe[prop-missing] - This is an object contained in a prop, but we don't know the exact type.
|
|
const propValue = props[propName];
|
|
addListenersToPropsValue(propValue, accumulator);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach`
|
|
* and `__detach`. However, this is more complicated because `AnimatedProps`
|
|
* uses reference counting to determine when to recursively detach its children
|
|
* nodes. So in order to optimize this, we avoid detaching until the next attach
|
|
* unless we are unmounting.
|
|
*/
|
|
function useAnimatedPropsLifecycle_layoutEffects(node: AnimatedProps): void {
|
|
const prevNodeRef = useRef<?AnimatedProps>(null);
|
|
const isUnmountingRef = useRef<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
// It is ok for multiple components to call `flushQueue` because it noops
|
|
// if the queue is empty. When multiple animated components are mounted at
|
|
// the same time. Only first component flushes the queue and the others will noop.
|
|
NativeAnimatedHelper.API.flushQueue();
|
|
let drivenAnimationEndedListener: ?EventSubscription = null;
|
|
if (node.__isNative) {
|
|
drivenAnimationEndedListener =
|
|
NativeAnimatedHelper.nativeEventEmitter.addListener(
|
|
'onUserDrivenAnimationEnded',
|
|
data => {
|
|
node.update();
|
|
},
|
|
);
|
|
}
|
|
|
|
return () => {
|
|
drivenAnimationEndedListener?.remove();
|
|
};
|
|
});
|
|
|
|
useLayoutEffect(() => {
|
|
isUnmountingRef.current = false;
|
|
return () => {
|
|
isUnmountingRef.current = true;
|
|
};
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
node.__attach();
|
|
if (prevNodeRef.current != null) {
|
|
const prevNode = prevNodeRef.current;
|
|
// TODO: Stop restoring default values (unless `reset` is called).
|
|
prevNode.__restoreDefaultValues();
|
|
prevNode.__detach();
|
|
prevNodeRef.current = null;
|
|
}
|
|
return () => {
|
|
if (isUnmountingRef.current) {
|
|
// NOTE: Do not restore default values on unmount, see D18197735.
|
|
node.__detach();
|
|
} else {
|
|
prevNodeRef.current = node;
|
|
}
|
|
};
|
|
}, [node]);
|
|
}
|
|
|
|
/**
|
|
* Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach`
|
|
* and `__detach`. However, this is more complicated because `AnimatedProps`
|
|
* uses reference counting to determine when to recursively detach its children
|
|
* nodes. So in order to optimize this, we avoid detaching until the next attach
|
|
* unless we are unmounting.
|
|
*/
|
|
function useAnimatedPropsLifecycle_insertionEffects(node: AnimatedProps): void {
|
|
const prevNodeRef = useRef<?AnimatedProps>(null);
|
|
const isUnmountingRef = useRef<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
// It is ok for multiple components to call `flushQueue` because it noops
|
|
// if the queue is empty. When multiple animated components are mounted at
|
|
// the same time. Only first component flushes the queue and the others will noop.
|
|
NativeAnimatedHelper.API.flushQueue();
|
|
});
|
|
|
|
useInsertionEffect(() => {
|
|
isUnmountingRef.current = false;
|
|
return () => {
|
|
isUnmountingRef.current = true;
|
|
};
|
|
}, []);
|
|
|
|
useInsertionEffect(() => {
|
|
node.__attach();
|
|
let drivenAnimationEndedListener: ?EventSubscription = null;
|
|
|
|
if (node.__isNative) {
|
|
drivenAnimationEndedListener =
|
|
NativeAnimatedHelper.nativeEventEmitter.addListener(
|
|
'onUserDrivenAnimationEnded',
|
|
data => {
|
|
node.update();
|
|
},
|
|
);
|
|
}
|
|
if (prevNodeRef.current != null) {
|
|
const prevNode = prevNodeRef.current;
|
|
// TODO: Stop restoring default values (unless `reset` is called).
|
|
prevNode.__restoreDefaultValues();
|
|
prevNode.__detach();
|
|
prevNodeRef.current = null;
|
|
}
|
|
return () => {
|
|
if (isUnmountingRef.current) {
|
|
// NOTE: Do not restore default values on unmount, see D18197735.
|
|
node.__detach();
|
|
} else {
|
|
prevNodeRef.current = node;
|
|
}
|
|
|
|
drivenAnimationEndedListener?.remove();
|
|
};
|
|
}, [node]);
|
|
}
|
|
|
|
function getEventTarget<TInstance>(instance: TInstance): TInstance {
|
|
return typeof instance === 'object' &&
|
|
typeof instance?.getScrollableNode === 'function'
|
|
? // $FlowFixMe[incompatible-use] - Legacy instance assumptions.
|
|
instance.getScrollableNode()
|
|
: instance;
|
|
}
|
|
|
|
// $FlowFixMe[unclear-type] - Legacy instance assumptions.
|
|
function isFabricInstance(instance: any): boolean {
|
|
return (
|
|
isFabricPublicInstance(instance) ||
|
|
// Some components have a setNativeProps function but aren't a host component
|
|
// such as lists like FlatList and SectionList. These should also use
|
|
// forceUpdate in Fabric since setNativeProps doesn't exist on the underlying
|
|
// host component. This crazy hack is essentially special casing those lists and
|
|
// ScrollView itself to use forceUpdate in Fabric.
|
|
// If these components end up using forwardRef then these hacks can go away
|
|
// as instance would actually be the underlying host component and the above check
|
|
// would be sufficient.
|
|
isFabricPublicInstance(instance?.getNativeScrollRef?.()) ||
|
|
isFabricPublicInstance(
|
|
instance?.getScrollResponder?.()?.getNativeScrollRef?.(),
|
|
)
|
|
);
|
|
}
|