Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions packages/react-native/Libraries/Animated/nodes/AnimatedObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import * as React from 'react';

const MAX_DEPTH = 5;

/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype
and ReactElement checks preserve the type refinement of `value`. */
function isPlainObject(value: mixed): value is $ReadOnly<{[string]: mixed}> {
export function isPlainObject(
value: mixed,
/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype
and ReactElement checks preserve the type refinement of `value`. */
): value is $ReadOnly<{[string]: mixed}> {
return (
value !== null &&
typeof value === 'object' &&
Expand Down Expand Up @@ -109,6 +111,14 @@ export default class AnimatedObject extends AnimatedWithChildren {
});
}

__getValueWithStaticObject(staticObject: mixed): any {
const nodes = this.#nodes;
let index = 0;
// NOTE: We can depend on `this._value` and `staticObject` sharing a
// structure because of `useAnimatedPropsMemo`.
return mapAnimatedNodes(staticObject, () => nodes[index++].__getValue());
}

__getAnimatedValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getAnimatedValue();
Expand Down
25 changes: 25 additions & 0 deletions packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ export default class AnimatedProps extends AnimatedNode {
return props;
}

/**
* Creates a new `props` object that contains the same props as the supplied
* `staticProps` object, except with animated nodes for any props that were
* created by this `AnimatedProps` instance.
*/
__getValueWithStaticProps(staticProps: Object): Object {
const props: {[string]: mixed} = {...staticProps};

const keys = Object.keys(staticProps);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const maybeNode = this.#props[key];

if (key === 'style' && maybeNode instanceof AnimatedStyle) {
props[key] = maybeNode.__getValueWithStaticStyle(staticProps.style);
} else if (maybeNode instanceof AnimatedNode) {
props[key] = maybeNode.__getValue();
} else if (maybeNode instanceof AnimatedEvent) {
props[key] = maybeNode.__getHandler();
}
}

return props;
}

__getAnimatedValue(): Object {
const props: {[string]: mixed} = {};

Expand Down
37 changes: 37 additions & 0 deletions packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,43 @@ export default class AnimatedStyle extends AnimatedWithChildren {
return Platform.OS === 'web' ? [this.#inputStyle, style] : style;
}

/**
* Creates a new `style` object that contains the same style properties as
* the supplied `staticStyle` object, except with animated nodes for any
* style properties that were created by this `AnimatedStyle` instance.
*/
__getValueWithStaticStyle(staticStyle: Object): Object | Array<Object> {
const flatStaticStyle = flattenStyle(staticStyle);
const style: {[string]: mixed} =
flatStaticStyle == null
? {}
: flatStaticStyle === staticStyle
? // Copy the input style, since we'll mutate it below.
{...flatStaticStyle}
: // Reuse `flatStaticStyle` if it is a newly created object.
flatStaticStyle;

const keys = Object.keys(style);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const maybeNode = this.#style[key];

if (key === 'transform' && maybeNode instanceof AnimatedTransform) {
style[key] = maybeNode.__getValueWithStaticTransforms(
// NOTE: This check should not be necessary, but the types are not
// enforced as of this writing.
Array.isArray(style[key]) ? style[key] : [],
);
} else if (maybeNode instanceof AnimatedObject) {
style[key] = maybeNode.__getValueWithStaticObject(style[key]);
} else if (maybeNode instanceof AnimatedNode) {
style[key] = maybeNode.__getValue();
}
}

return Platform.OS === 'web' ? [this.#inputStyle, style] : style;
}

__getAnimatedValue(): Object {
const style: {[string]: mixed} = {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ export default class AnimatedTransform extends AnimatedWithChildren {
);
}

__getValueWithStaticTransforms(
staticTransforms: $ReadOnlyArray<Object>,
): $ReadOnlyArray<Object> {
const values = [];
mapTransforms(this._transforms, node => {
values.push(node.__getValue());
});
// NOTE: We can depend on `this._transforms` and `staticTransforms` sharing
// a structure because of `useAnimatedPropsMemo`.
return mapTransforms(staticTransforms, () => values.shift());
}

__getAnimatedValue(): $ReadOnlyArray<Transform<any>> {
return mapTransforms(this._transforms, animatedNode =>
animatedNode.__getAnimatedValue(),
Expand Down
26 changes: 17 additions & 9 deletions packages/react-native/Libraries/Animated/useAnimatedProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
useReducer,
useRef,
} from 'react';
import {useAnimatedPropsMemo} from '../../src/private/animated/useAnimatedPropsMemo';

type ReducedProps<TProps> = {
...TProps,
Expand All @@ -40,23 +41,24 @@ type AnimatedValueListeners = Array<{
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<?() => void>(null);
const onUpdateRef = useRef<(() => void) | null>(null);
const timerRef = useRef<TimeoutID | null>(null);

const allowlistIfEnabled = ReactNativeFeatureFlags.enableAnimatedAllowlist()
? allowlist
: 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(
const node = useMemoOrAnimatedPropsMemo(
() =>
new AnimatedProps(
props,
Expand All @@ -65,6 +67,7 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
),
[allowlistIfEnabled, props],
);

const useNativePropsInFabric =
ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric();
const useSetNativePropsInNativeAnimationsInFabric =
Expand Down Expand Up @@ -204,14 +207,19 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
);
const callbackRef = useRefEffect<TInstance>(refEffect);

return [reduceAnimatedProps<TProps>(node), callbackRef];
return [reduceAnimatedProps<TProps>(node, props), callbackRef];
}

function reduceAnimatedProps<TProps>(node: AnimatedNode): ReducedProps<TProps> {
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 {
...node.__getValue(),
...(ReactNativeFeatureFlags.enableAnimatedPropsMemo()
? node.__getValueWithStaticProps(props)
: node.__getValue()),
collapsable: false,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -938,11 +938,15 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
`;

exports[`public API should not change unintentionally Libraries/Animated/nodes/AnimatedObject.js 1`] = `
"declare export default class AnimatedObject extends AnimatedWithChildren {
"declare export function isPlainObject(
value: mixed
): value is $ReadOnly<{ [string]: mixed }>;
declare export default class AnimatedObject extends AnimatedWithChildren {
_value: mixed;
static from(value: mixed): ?AnimatedObject;
constructor(nodes: $ReadOnlyArray<AnimatedNode>, value: mixed): void;
__getValue(): any;
__getValueWithStaticObject(staticObject: mixed): any;
__getAnimatedValue(): any;
__attach(): void;
__detach(): void;
Expand All @@ -964,6 +968,7 @@ declare export default class AnimatedProps extends AnimatedNode {
allowlist?: ?AnimatedPropsAllowlist
): void;
__getValue(): Object;
__getValueWithStaticProps(staticProps: Object): Object;
__getAnimatedValue(): Object;
__attach(): void;
__detach(): void;
Expand Down Expand Up @@ -992,6 +997,7 @@ declare export default class AnimatedStyle extends AnimatedWithChildren {
inputStyle: any
): void;
__getValue(): Object | Array<Object>;
__getValueWithStaticStyle(staticStyle: Object): Object | Array<Object>;
__getAnimatedValue(): Object;
__attach(): void;
__detach(): void;
Expand Down Expand Up @@ -1061,6 +1067,9 @@ declare export default class AnimatedTransform extends AnimatedWithChildren {
): void;
__makeNative(platformConfig: ?PlatformConfig): void;
__getValue(): $ReadOnlyArray<Transform<any>>;
__getValueWithStaticTransforms(
staticTransforms: $ReadOnlyArray<Object>
): $ReadOnlyArray<Object>;
__getAnimatedValue(): $ReadOnlyArray<Transform<any>>;
__attach(): void;
__detach(): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ const definitions: FeatureFlagDefinitions = {
defaultValue: false,
description: 'Enables Animated to skip non-allowlisted props and styles.',
},
enableAnimatedPropsMemo: {
defaultValue: false,
description:
'Enables Animated to analyze props to minimize invalidating `AnimatedProps`.',
},
enableOptimisedVirtualizedCells: {
defaultValue: false,
description:
Expand Down
Loading