mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Animated: Support Concurrent Rendering
Summary: Creates a `useAnimatedProps` hook that is compatible with concurrent mode, and uses this in `createAnimatedComponent_EXPERIMENTAL`. Changelog: [Internal] Reviewed By: lunaleaps Differential Revision: D28390393 fbshipit-source-id: d8aa471507a5d096832b93caf63a24f71733deda
This commit is contained in:
committed by
Facebook GitHub Bot
parent
cb25638a0e
commit
c774f9e97e
@@ -8,6 +8,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import useAnimatedProps from './useAnimatedProps';
|
||||
import useMergeRefs from '../Utilities/useMergeRefs';
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
@@ -17,7 +19,12 @@ import * as React from 'react';
|
||||
export default function createAnimatedComponent<TProps: {...}, TInstance>(
|
||||
Component: React.AbstractComponent<TProps, TInstance>,
|
||||
): React.AbstractComponent<TProps, TInstance> {
|
||||
return React.forwardRef((props, ref) => {
|
||||
throw new Error('createAnimatedComponent: Not yet implemented.');
|
||||
return React.forwardRef((props, forwardedRef) => {
|
||||
const [reducedProps, callbackRef] = useAnimatedProps<TProps, TInstance>(
|
||||
props,
|
||||
);
|
||||
const ref = useMergeRefs<TInstance | null>(callbackRef, forwardedRef);
|
||||
|
||||
return <Component {...reducedProps} ref={ref} />;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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';
|
||||
import {useCallback, useLayoutEffect, useMemo, useReducer, useRef} from 'react';
|
||||
|
||||
type ReducedProps<TProps> = {
|
||||
...TProps,
|
||||
collapsable: boolean,
|
||||
...
|
||||
};
|
||||
type CallbackRef<T> = T => mixed;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user