react-native-reanimated icon indicating copy to clipboard operation
react-native-reanimated copied to clipboard

Animated.View with entering => children no longer visible in 3.19.1

Open pierroo opened this issue 3 months ago • 13 comments

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

pierroo avatar Sep 01 '25 13:09 pierroo

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.

github-actions[bot] avatar Sep 01 '25 13:09 github-actions[bot]

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?

github-actions[bot] avatar Sep 01 '25 13:09 github-actions[bot]

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

pierroo avatar Sep 01 '25 14:09 pierroo

Could you provide a reproduction in a clean setup to confirm whether something else might be affecting this problem?

piaskowyk avatar Sep 05 '25 09:09 piaskowyk

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>

pierroo avatar Sep 05 '25 09:09 pierroo

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.

pierroo avatar Sep 08 '25 10:09 pierroo

Same with reanimated 4.0.2, new arch. Happens with FadeIn animation.

RohovDmytro avatar Sep 16 '25 12:09 RohovDmytro

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}

RohovDmytro avatar Sep 16 '25 12:09 RohovDmytro

Okay. It appears to be this specific combination does not work:

<Animated.View entering={BounceIn} exiting={undefined} style={props.style}>
      {props.children}
</Animated.View>

RohovDmytro avatar Sep 16 '25 12:09 RohovDmytro

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

Vishal-D4 avatar Sep 17 '25 07:09 Vishal-D4

<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 avatar Sep 29 '25 14:09 vokhuyetOz

@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

Vishal-D4 avatar Sep 30 '25 05:09 Vishal-D4

I am facing this as well

Nasseratic avatar Nov 04 '25 14:11 Nasseratic