react-native-macos/Libraries/Animated/useAnimatedProps.js

212 строки
7.0 KiB
JavaScript
Исходник Обычный вид История

/**
* Copyright (c) Facebook, Inc. and its 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
*/
'use strict';
import AnimatedProps from './nodes/AnimatedProps';
import {AnimatedEvent} from './AnimatedEvent';
import useRefEffect from '../Utilities/useRefEffect';
Add animation queue to modern createAnimatedComponent Summary: Add animation queuing back into createAnimatedComponent_EXPERIMENTAL.js, which is a concurrent-safe version of createAnimatedComponent. T93269035 for details on why this is needed. # How does this work? In the old createAnimatedComponent, Animations were queued by calling `setWaitingForIdentifier` before render, and then calling `unsetWaitingForIdentifier` after render. In this diff, instead we are calling `setWaitingForIdentifier` in an `useLayoutEffect` before calling `useAnimatedProps`, and we are calling `unsetWaitingForIdentifier` in a `useEffect` after `useAnimatedProps`. So the ordering for the effects are: 1. `useLayoutEffect` with `setWaiting` 2. `useLayoutEffect`s in `useAnimatedProps` 3. `useEffect`s in `useAnimatedProps` 4. `useEffect` with `unsetWaiting`. There's a React guarantee that **if one effect is called, all of them will be called**, so we don't have a concern about the queue getting locked. ## **Main concerns:** 1. This works in my test cases, but it's not the same behavior as the old createAnimatedComponent (which is wait before and unset wait after render). This may still be ok because the relevant side effects in render from that component have been moved to `useLayoutEffect` or `useEffect` in `useAnimatedProps` (so the ordering is still the same?). 2. I'm not sure about the ordering of `onLayoutEffect`, `onLayout` callbacks, and `useEffect`. createAnimatedComponent_EXPERIMENTAL doesn't use `onLayout`, but with this new method of queuing, **`onLayout` calls will likely be called before the animation queue has been flushed**. It's not clear to me whether this is bad. Changelog: [Internal] Reviewed By: yungsters Differential Revision: D29467458 fbshipit-source-id: 2be23a8968404526d0fa394a7879fda8b5ffbfdc
2021-07-08 19:28:18 +03:00
import NativeAnimatedHelper from './NativeAnimatedHelper';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react';
type ReducedProps<TProps> = {
...TProps,
collapsable: boolean,
...
};
type CallbackRef<T> = T => mixed;
Add animation queue to modern createAnimatedComponent Summary: Add animation queuing back into createAnimatedComponent_EXPERIMENTAL.js, which is a concurrent-safe version of createAnimatedComponent. T93269035 for details on why this is needed. # How does this work? In the old createAnimatedComponent, Animations were queued by calling `setWaitingForIdentifier` before render, and then calling `unsetWaitingForIdentifier` after render. In this diff, instead we are calling `setWaitingForIdentifier` in an `useLayoutEffect` before calling `useAnimatedProps`, and we are calling `unsetWaitingForIdentifier` in a `useEffect` after `useAnimatedProps`. So the ordering for the effects are: 1. `useLayoutEffect` with `setWaiting` 2. `useLayoutEffect`s in `useAnimatedProps` 3. `useEffect`s in `useAnimatedProps` 4. `useEffect` with `unsetWaiting`. There's a React guarantee that **if one effect is called, all of them will be called**, so we don't have a concern about the queue getting locked. ## **Main concerns:** 1. This works in my test cases, but it's not the same behavior as the old createAnimatedComponent (which is wait before and unset wait after render). This may still be ok because the relevant side effects in render from that component have been moved to `useLayoutEffect` or `useEffect` in `useAnimatedProps` (so the ordering is still the same?). 2. I'm not sure about the ordering of `onLayoutEffect`, `onLayout` callbacks, and `useEffect`. createAnimatedComponent_EXPERIMENTAL doesn't use `onLayout`, but with this new method of queuing, **`onLayout` calls will likely be called before the animation queue has been flushed**. It's not clear to me whether this is bad. Changelog: [Internal] Reviewed By: yungsters Differential Revision: D29467458 fbshipit-source-id: 2be23a8968404526d0fa394a7879fda8b5ffbfdc
2021-07-08 19:28:18 +03:00
let animatedComponentNextId = 1;
export default function useAnimatedProps<TProps: {...}, TInstance>(
props: TProps,
): [ReducedProps<TProps>, CallbackRef<TInstance | null>] {
const [, scheduleUpdate] = useReducer(count => count + 1, 0);
const onUpdateRef = useRef<?() => void>(null);
// TODO: Only invalidate `node` if animated props or `style` change. In the
// previous implementation, we permitted `style` to override props with the
// same name property name as styles, so we can probably continue doing that.
// The ordering of other props *should* not matter.
const node = useMemo(
() => new AnimatedProps(props, () => onUpdateRef.current?.()),
[props],
);
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 separat "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 => {
// NOTE: This may be called more often than necessary (e.g. when `props`
// changes), but `setNativeView` already optimizes for that.
node.setNativeView(instance);
// NOTE: This callback is only used by the JavaScript animation driver.
onUpdateRef.current = () => {
if (
process.env.NODE_ENV === 'test' ||
typeof instance !== 'object' ||
typeof instance?.setNativeProps !== 'function' ||
isFabricInstance(instance)
) {
// Schedule an update for this component to update `reducedProps`,
// but do not compute it immediately. If a parent also updated, we
// need to merge those new props in before updating.
scheduleUpdate();
} else if (!node.__isNative) {
// $FlowIgnore[not-a-function] - Assume it's still a function.
instance.setNativeProps(node.__getAnimatedValue());
} else {
throw new Error(
'Attempting to run JS driven animation on animated node ' +
'that has been moved to "native" earlier by starting an ' +
'animation with `useNativeDriver: true`',
);
}
};
const target = getEventTarget(instance);
const events = [];
for (const propName in props) {
const propValue = props[propName];
if (propValue instanceof AnimatedEvent && propValue.__isNative) {
propValue.__attach(target, propName);
events.push([propName, propValue]);
}
}
return () => {
onUpdateRef.current = null;
for (const [propName, propValue] of events) {
propValue.__detach(target, propName);
}
};
},
[props, node],
);
const callbackRef = useRefEffect<TInstance>(refEffect);
return [reduceAnimatedProps<TProps>(node), callbackRef];
}
function reduceAnimatedProps<TProps>(
node: AnimatedProps,
): 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 {
...node.__getValue(),
collapsable: false,
};
}
/**
* 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(node: AnimatedProps): void {
const prevNodeRef = useRef<?AnimatedProps>(null);
const isUnmountingRef = useRef<boolean>(false);
Add animation queue to modern createAnimatedComponent Summary: Add animation queuing back into createAnimatedComponent_EXPERIMENTAL.js, which is a concurrent-safe version of createAnimatedComponent. T93269035 for details on why this is needed. # How does this work? In the old createAnimatedComponent, Animations were queued by calling `setWaitingForIdentifier` before render, and then calling `unsetWaitingForIdentifier` after render. In this diff, instead we are calling `setWaitingForIdentifier` in an `useLayoutEffect` before calling `useAnimatedProps`, and we are calling `unsetWaitingForIdentifier` in a `useEffect` after `useAnimatedProps`. So the ordering for the effects are: 1. `useLayoutEffect` with `setWaiting` 2. `useLayoutEffect`s in `useAnimatedProps` 3. `useEffect`s in `useAnimatedProps` 4. `useEffect` with `unsetWaiting`. There's a React guarantee that **if one effect is called, all of them will be called**, so we don't have a concern about the queue getting locked. ## **Main concerns:** 1. This works in my test cases, but it's not the same behavior as the old createAnimatedComponent (which is wait before and unset wait after render). This may still be ok because the relevant side effects in render from that component have been moved to `useLayoutEffect` or `useEffect` in `useAnimatedProps` (so the ordering is still the same?). 2. I'm not sure about the ordering of `onLayoutEffect`, `onLayout` callbacks, and `useEffect`. createAnimatedComponent_EXPERIMENTAL doesn't use `onLayout`, but with this new method of queuing, **`onLayout` calls will likely be called before the animation queue has been flushed**. It's not clear to me whether this is bad. Changelog: [Internal] Reviewed By: yungsters Differential Revision: D29467458 fbshipit-source-id: 2be23a8968404526d0fa394a7879fda8b5ffbfdc
2021-07-08 19:28:18 +03:00
const [animatedComponentId] = useState(
() => `${animatedComponentNextId++}:animatedComponent`,
);
useLayoutEffect(() => {
NativeAnimatedHelper.API.setWaitingForIdentifier(animatedComponentId);
});
useEffect(() => {
NativeAnimatedHelper.API.unsetWaitingForIdentifier(animatedComponentId);
});
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]);
}
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 (
hasFabricHandle(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.
hasFabricHandle(instance?.getNativeScrollRef?.()) ||
hasFabricHandle(instance?.getScrollResponder?.()?.getNativeScrollRef?.())
);
}
// $FlowFixMe[unclear-type] - Legacy instance assumptions.
function hasFabricHandle(instance: any): boolean {
// eslint-disable-next-line dot-notation
return instance?.['_internalInstanceHandle']?.stateNode?.canonical != null;
}