Animated.View with entering => children no longer visible in 3.19.1
Description
Upgraded from 3.19.0 to 3.19.1, and now all my <Animated.View that have the entering/exiting props will no longer show their content.
although the content remains "active/clickable", it's no longer visible at all.
Absolute mystery.
Steps to reproduce
upgrade to 3.19.1 use an animated view with entering props you can no longer view its children
Snack or a link to a repository
**
Reanimated version
3.19.1
Worklets version
none
React Native version
0.81.1
Platforms
Android
JavaScript runtime
None
Workflow
React Native CLI
Architecture
New Architecture (Fabric renderer)
Build type
No response
Device
Android emulator
Host machine
None
Device model
No response
Acknowledgements
Yes
Hey! 👋
It looks like you've omitted a few important sections from the issue template.
Please complete Snack or a link to a repository section.
Hey! 👋
The issue doesn't seem to contain a minimal reproduction.
Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?
EDIT: apparently it's react native 0.81.1 that triggers this issue, not reanimated 3.19.1 (because I rolledback to 3.19.0 and it still happens, only other change was upgrading RN from 0.80.1 to 0.81.1)
Meaning buildToolsVersion=36, compileSdkVersion=36 as per react native upgrade helper (if that helps)
I leave the issue opened since obviously it does show some incompatibility with latest react native
Could you provide a reproduction in a clean setup to confirm whether something else might be affecting this problem?
Nothing else seems to be affecting this, using the configuration needed for RN 0.81.1, with latest reanimated, and a simple component for example if you place this next to a textinput to simulate the usual "send button or options icons buttons etc" like so will show the issue:
<TextInput
multiline
value={value}
style={[{
paddingRight: 90,
paddingLeft: 50,
paddingVertical: 9,
maxHeight: 124,
minHeight: 46,
borderRadius: 24,
maxWidth: '100%',
minWidth: '100%',
borderColor: BORDER_COLOR,
borderWidth: 1,
},
/>
<View style={{
overflow: 'hidden', position: 'absolute', right: 0,
bottom: 2,
height: 45,
alignSelf: 'flex-end', zIndex: 9999,
justifyContent: 'center'
}}>
{
value.trim().length > 0 && (
<Animated.View entering={entering} exiting={exiting}>
<Button >
<Text >Send</Text>
</Button>
</Animated.View>
)
}
{
!(value.trim().length > 0) && (
<Animated.View entering={entering} exiting={exiting}>
<RightButtons />
</Animated.View>
)
}
</View>
I come with more info, since obviously I understand my report might not have enough elements for you to replicate:
This issue only happens with entering/exiting using "ZoomIn" / "ZoomOut" animations. the "FadeIn ones work as expected.
Same with reanimated 4.0.2, new arch. Happens with FadeIn animation.
Using this component breaks layout animations:
type Props = {
children?: any;
entering?: ComponentProps<typeof Animated.View>['entering'] | null;
exiting?: ComponentProps<typeof Animated.View>['exiting'] | null;
/** Might be an animated style */
style?: ViewProps['style'];
onLayout?: ViewProps['onLayout'];
};
export const ReanimatedExit = (props: Props) => {
const entering =
props.entering === null
? undefined
: props.entering || ANIMATION_CONFIG_REFRESH_QUICK;
const exiting =
props.exiting === null
? undefined
: props.exiting || ANIMATION_CONFIG_QUICK_REFRESH_EXIT;
if (FLAG.TRUE) {
return (
<Animated.View
onLayout={props.onLayout}
testID={TEST_ID.REANIMATED_EXIT}
entering={entering}
exiting={exiting}
style={props.style}
>
{props.children}
</Animated.View>
);
}
return (
<View onLayout={props.onLayout} testID={TEST_ID.REANIMATED_EXIT} style={props.style}>
{props.children}
</View>
);
};
Removing entering / existing definitions and using things like this does work:
entering={ANIMATION_CONFIG_REFRESH_QUICK}
exiting={ANIMATION_CONFIG_REFRESH_QUICK}
Okay. It appears to be this specific combination does not work:
<Animated.View entering={BounceIn} exiting={undefined} style={props.style}>
{props.children}
</Animated.View>
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {
View,
Pressable,
ViewStyle,
Dimensions,
LayoutChangeEvent,
Platform,
} from 'react-native';
import {createStyleSheet, useStyles} from 'react-native-unistyles';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
interpolate,
Easing,
} from 'react-native-reanimated';
import { runOnJS } from 'react-native-worklets';
import {Portal} from '@gorhom/portal';
type Placement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'right';
interface DropdownProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
trigger: React.ReactNode;
containerStyle?: ViewStyle;
placement?: Placement;
offset?: number;
}
interface Position {
top: number;
left: number;
}
interface ContentDimensions {
width: number;
height: number;
}
export default function Dropdown({
isOpen,
onClose,
children,
trigger,
containerStyle,
placement = 'bottom',
offset = 8,
}: DropdownProps) {
const {styles} = useStyles(stylesheet);
const animation = useSharedValue(0);
const triggerRef = useRef<View>(null);
const [position, setPosition] = useState<Position>({top: 0, left: 0});
const [contentDimensions, setContentDimensions] = useState<ContentDimensions>(
{
width: 200,
height: 0,
},
);
const [isVisible, setIsVisible] = useState(false);
const windowDimensions = Dimensions.get('window');
console.log(isOpen, 'isOpen');
const isIos = Platform.OS === 'ios';
const handleContentLayout = useCallback((event: LayoutChangeEvent) => {
const {width, height} = event.nativeEvent.layout;
setContentDimensions({width, height});
}, []);
const measureTrigger = useCallback(() => {
if (triggerRef.current) {
triggerRef.current.measureInWindow((x, y, width, height) => {
// Calculate position based on placement
let newPosition: Position = {top: 0, left: 0};
const contentWidth = contentDimensions.width;
const contentHeight = contentDimensions.height;
switch (placement) {
case 'bottom':
newPosition = {
top: y + height + offset,
left: x + width / 2 - contentWidth / 2,
};
break;
case 'bottom-start':
newPosition = {
top: y + height + offset,
left: x,
};
break;
case 'bottom-end':
newPosition = {
top: y + height + offset,
left: x + width - contentWidth,
};
break;
case 'top':
newPosition = {
top: y - offset - contentHeight,
left: x + width / 2 - contentWidth / 2,
};
break;
case 'top-start':
newPosition = {
top: y - offset - contentHeight,
left: x,
};
break;
case 'top-end':
newPosition = {
top: y - offset - contentHeight,
left: x + width - contentWidth,
};
break;
case 'left':
newPosition = {
top: y + height / 2 - contentHeight / 2,
left: x - offset - contentWidth,
};
break;
case 'right':
newPosition = {
top: y + height / 2 - contentHeight / 2,
left: x + width + offset,
};
break;
}
// Adjust position to keep within screen bounds
if (newPosition.left < 0) {
newPosition.left = offset;
} else if (newPosition.left + contentWidth > windowDimensions.width) {
newPosition.left = windowDimensions.width - contentWidth - offset;
}
if (newPosition.top < 0) {
newPosition.top = offset;
} else if (newPosition.top + contentHeight > windowDimensions.height) {
newPosition.top = windowDimensions.height - contentHeight - offset;
}
setPosition(newPosition);
});
}
}, [placement, offset, windowDimensions, contentDimensions]);
const handleAnimationComplete = useCallback((isOpening: boolean) => {
if (!isOpening) {
setIsVisible(false);
}
}, []);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
measureTrigger();
animation.value = withTiming(1, {
duration: 300,
easing: Easing.bezier(0.34, 1.56, 0.64, 1),
});
} else {
animation.value = withTiming(
0,
{
duration: 150,
easing: Easing.bezier(0.4, 0, 0.2, 1),
},
finished => {
if (finished) {
runOnJS(handleAnimationComplete)(false);
}
},
);
}
}, [isOpen, measureTrigger, animation, handleAnimationComplete]);
const animatedStyles = useAnimatedStyle(() => {
const opacity = interpolate(animation.value, [0, 1], [0, 1]);
const scale = interpolate(animation.value, [0, 1], [0.9, 1]);
let translateY = 0;
let translateX = 0;
if (placement.startsWith('bottom')) {
translateY = interpolate(animation.value, [0, 1], [-20, isIos ? 0 : 40]);
} else if (placement.startsWith('top')) {
translateY = interpolate(animation.value, [0, 1], [20, 0]);
} else if (placement === 'left') {
translateX = interpolate(animation.value, [0, 1], [20, 0]);
} else if (placement === 'right') {
translateX = interpolate(animation.value, [0, 1], [-20, 0]);
}
return {
opacity,
transform: [{scale}, {translateX}, {translateY}],
};
});
console.log(isVisible, 'isVisible');
return (
<>
<View ref={triggerRef}>{trigger}</View>
{isVisible && (
<Portal>
<Pressable style={styles.overlay} onPress={onClose}>
<Animated.View
onLayout={handleContentLayout}
style={[
styles.contentWrapper,
animatedStyles,
{
position: 'absolute',
top: position.top,
left: position.left,
},
]}
pointerEvents="box-none">
<View style={[styles.content, containerStyle]}>{children}</View>
</Animated.View>
</Pressable>
</Portal>
)}
</>
);
}
const stylesheet = createStyleSheet(theme => ({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'transparent',
},
contentWrapper: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
zIndex: 1000,
},
content: {
backgroundColor: theme.colors.surface,
borderRadius: theme.borderRadius.md,
minWidth: 200,
overflow: 'hidden',
},
}));
"react-native"-: 0.81.4 "react-native-reanimated"-: 4.0.0 "react-native-worklets": "^0.5.1"
I am having same issue where the children are not visible, even though i am not using enter and exit animation. Please provide a solution to fix this ASAP
<Animated.View
style={{
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: 'red',
}}
entering={SlideInLeft}
>
<AppButtonNormal
buttonSize="small"
title={Strings.First_chapter}
onPress={onPress(list.data[list.data.length - 1])}
/>
</Animated.View>
in iOS, it worked as expect. in Android, Children compoent is not visible, although i can click it
@vokhuyetOz i couldn't find the exact issue but found a workaround by wrapping my component into a Modal. Although it can create some issue if you are rendering bottomSheet or another modal after pressing on any child, but it can be fixed
I am facing this as well