Skip to content
Merged
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
216 changes: 216 additions & 0 deletions app/src/examples/DynamicStylesExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { Text, StyleSheet, View, Button } from 'react-native';

import React from 'react';

import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';

export default function DynamicStylesExample() {
const [extraStyle, setExtraStyle] = React.useState(false);
const [rerender, setRerender] = React.useState(false);

const widthShared = useSharedValue(80);
const backgroundColorShared = useSharedValue<string>('rgb(255,127,63)');

const animatedWidthStyle = useAnimatedStyle(() => ({
width: widthShared.value,
}));
const animatedBackgroundColorStyle = useAnimatedStyle(() => {
return {
backgroundColor: backgroundColorShared.value,
};
});

function handleChangeProps() {
setExtraStyle((extraStyle) => !extraStyle);
}

function handleAnimate() {
widthShared.value = withTiming((Math.random() * 160) / 2 + 40, {
duration: 1000,
});

const r = Math.random() * 256;
const g = Math.random() * 256;
const b = Math.random() * 256;

backgroundColorShared.value = withTiming(`rgb(${r},${g},${b})`, {
duration: 1500,
});
}

function handleForceRerender() {
setRerender(!rerender);
}

return (
<View style={styles.container}>
<Text style={styles.text}>State: {String(rerender)}</Text>
<View style={styles.columnContainer}>
<View style={styles.column}>
<Text style={styles.text}>InlineStyles</Text>

<Text style={styles.text}>Same length array</Text>
<Animated.View
accessible={rerender}
style={[
styles.box,
{ width: widthShared },
extraStyle ? { backgroundColor: backgroundColorShared } : null,
]}
/>

<Text style={styles.text}>Different length array</Text>
<Animated.View
accessible={rerender}
style={
extraStyle
? [
styles.box,
{ width: widthShared },
{ backgroundColor: backgroundColorShared },
]
: [styles.box, { width: widthShared }]
}
/>
<Text style={styles.text}>Same length extra plain style array</Text>
<Animated.View
accessible={rerender}
style={[
styles.box,
{ width: widthShared },
extraStyle ? { backgroundColor: backgroundColorShared } : null,
extraStyle ? styles.radius : null,
]}
/>
<Text style={styles.text}>
Different length extra plain style array
</Text>
<Animated.View
accessible={rerender}
style={
extraStyle
? [
styles.box,
{ width: widthShared },
{ backgroundColor: backgroundColorShared },
styles.radius,
]
: [styles.box, { width: widthShared }]
}
/>
<Text style={styles.text}>
Two animated styles independent of state
</Text>
<Animated.View
accessible={rerender}
style={[
styles.box,
{ width: widthShared },
{ backgroundColor: backgroundColorShared },
]}
/>
</View>
<View style={styles.column}>
<Text style={styles.text}>useAnimatedStyle</Text>

<Text style={styles.text}>Same length array</Text>
<Animated.View
accessible={rerender}
style={[
styles.box,
animatedWidthStyle,
extraStyle ? animatedBackgroundColorStyle : null,
]}
/>
<Text style={styles.text}>Different length array</Text>
<Animated.View
accessible={rerender}
style={
extraStyle
? [styles.box, animatedWidthStyle, animatedBackgroundColorStyle]
: [styles.box, animatedWidthStyle]
}
/>
<Text style={styles.text}>Same length extra plain style array</Text>
<Animated.View
accessible={rerender}
style={[
styles.box,
animatedWidthStyle,
extraStyle ? animatedBackgroundColorStyle : null,
extraStyle ? styles.radius : null,
]}
/>
<Text style={styles.text}>
Different length extra plain style array
</Text>
<Animated.View
accessible={rerender}
style={
extraStyle
? [
styles.box,
animatedWidthStyle,
animatedBackgroundColorStyle,
styles.radius,
]
: [styles.box, animatedWidthStyle]
}
/>
<Text style={styles.text}>
Two animated styles independent of state
</Text>
<Animated.View
accessible={rerender}
style={[
styles.box,
animatedWidthStyle,
animatedBackgroundColorStyle,
]}
/>
</View>
</View>

<Text style={styles.text}>
{extraStyle ? '' : 'no '}extra animated style
</Text>
<Button title="Add/remove animated style" onPress={handleChangeProps} />
<Button title="Force rerender" onPress={handleForceRerender} />
<Button title="Animate" onPress={handleAnimate} />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
columnContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
},
box: {
height: 50,
width: 50,
borderWidth: 1,
},
radius: {
borderWidth: 10,
},
column: {
maxWidth: '50%',
alignItems: 'center',
justifyContent: 'center',
},
text: {
maxWidth: 180,
textAlign: 'center',
},
});
6 changes: 6 additions & 0 deletions app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import WobbleExample from './WobbleExample';
import WorkletExample from './WorkletExample';
import WorkletRuntimeExample from './WorkletRuntimeExample';
import NestedLayoutAnimationConfig from './LayoutAnimations/NestedLayoutAnimationConfig';
import DynamicStylesExample from './DynamicStylesExample';
import WithClampExample from './WithClampExample';
import WorkletFactoryCrash from './WorkletFactoryCrashExample';
import RuntimeTestsExample from './RuntimeTests/RuntimeTestsExample';
Expand Down Expand Up @@ -465,6 +466,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'Log test',
screen: LogExample,
},
DynamicStylesExample: {
icon: '🧨',
title: 'Dynamically appending/removing styles',
screen: DynamicStylesExample,
},
WorkletFactoryCrash: {
icon: '🏭',
title: 'Worklet factory crash',
Expand Down
4 changes: 2 additions & 2 deletions src/createAnimatedComponent/InlinePropManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ export function hasInlineStyles(style: StyleProps): boolean {

export function getInlineStyle(
style: Record<string, unknown>,
isFirstRender: boolean
shouldGetInitialStyle: boolean
) {
if (isFirstRender) {
if (shouldGetInitialStyle) {
return getInlinePropsUpdate(style);
}
const newStyle: StyleProps = {};
Expand Down
31 changes: 28 additions & 3 deletions src/createAnimatedComponent/PropsFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
'use strict';

import { shallowEqual } from '../reanimated2/hook/utils';
import type { StyleProps } from '../reanimated2';
import { isSharedValue } from '../reanimated2';
import { isChromeDebugger } from '../reanimated2/PlatformChecker';
Expand All @@ -22,23 +24,32 @@ function dummyListener() {

export class PropsFilter implements IPropsFilter {
private _initialStyle = {};
private _previousProps: React.Component['props'] | null = null;
private _requiresNewInitials = true;

public filterNonAnimatedProps(
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
): Record<string, unknown> {
const inputProps =
component.props as AnimatedComponentProps<InitialComponentProps>;

this._maybePrepareForNewInitials(inputProps);

const props: Record<string, unknown> = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
const styleProp = inputProps.style;
const styles = flattenArray<StyleProps>(styleProp ?? []);
if (this._requiresNewInitials) {
this._initialStyle = {};
}
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
// TODO - refactor, since `viewsRef` is only present on Web
style.viewsRef?.add(component);
if (component._isFirstRender) {
if (this._requiresNewInitials) {
this._initialStyle = {
...style.initial.value,
...this._initialStyle,
Expand All @@ -47,7 +58,7 @@ export class PropsFilter implements IPropsFilter {
}
return this._initialStyle;
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, component._isFirstRender);
return getInlineStyle(style, this._requiresNewInitials);
} else {
return style;
}
Expand All @@ -61,6 +72,7 @@ export class PropsFilter implements IPropsFilter {
Object.keys(animatedProp.initial.value).forEach((initialValueKey) => {
props[initialValueKey] =
animatedProp.initial?.value[initialValueKey];
// TODO - refacotr, since `viewsRef` is only present on Web
animatedProp.viewsRef?.add(component);
});
}
Expand All @@ -80,13 +92,26 @@ export class PropsFilter implements IPropsFilter {
props[key] = dummyListener;
}
} else if (isSharedValue(value)) {
if (component._isFirstRender) {
if (this._requiresNewInitials) {
props[key] = value.value;
}
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
props[key] = value;
}
}
this._requiresNewInitials = false;
return props;
}

private _maybePrepareForNewInitials(
inputProps: AnimatedComponentProps<InitialComponentProps>
) {
if (this._previousProps && inputProps.style) {
this._requiresNewInitials = !shallowEqual(
this._previousProps,
inputProps
);
}
this._previousProps = inputProps;
}
}