Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RNR ver. => 3.16.x very freeze ios and android #6967

Open
denysoleksiienko opened this issue Jan 31, 2025 · 2 comments
Open

RNR ver. => 3.16.x very freeze ios and android #6967

denysoleksiienko opened this issue Jan 31, 2025 · 2 comments
Labels
Missing info The user didn't precise the problem enough Missing repro This issue need minimum repro scenario Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS

Comments

@denysoleksiienko
Copy link

denysoleksiienko commented Jan 31, 2025

Description

I have updated RNR from 3.15.1 to the latest 3.16.7 in my project and screens, where using animations very freezes which works on v. 3.15.1.
I don't know what happens, but it is hard to reproduce in a clear project without any project logic. I use the NX mono repo.
For example, this Accordion sometimes stacks expand, and many other components like Animated TextInpput.
I don't have problems with version 3.15.1 when I use both my custom components Animated TextInput and Accordion:

import React, { useCallback, useEffect, useState } from 'react';
import { LayoutChangeEvent, View } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  Easing,
  interpolate,
} from 'react-native-reanimated';

type EasingFunction = (amount: number) => number;

interface AccordionProps {
  expanded: boolean;
  easing?: EasingFunction;
  children: React.ReactNode;
  duration?: number;
}

export function Accordion({
  expanded = false,
  duration = 300,
  easing,
  children,
}: AccordionProps) {
  const animatedHeight = useSharedValue(0);
  const [contentHeight, setContentHeight] = useState<number | null>(null);

  const animatedStyle = useAnimatedStyle(() => ({
    height: animatedHeight.value,
    opacity: interpolate(animatedHeight.value, [0, contentHeight ?? 1], [0, 1]),
  }));

  const updateHeightAnimation = useCallback(
    (targetHeight: number) => {
      animatedHeight.value = withTiming(targetHeight, {
        duration,
        easing: easing || Easing.bezier(0.25, 0.1, 0.25, 1),
      });
    },
    [animatedHeight, duration, easing],
  );

  useEffect(() => {
    if (contentHeight !== null) {
      const targetHeight = expanded ? contentHeight : 0;
      updateHeightAnimation(targetHeight);
    }
  }, [expanded, contentHeight, updateHeightAnimation]);

  const handleLayout = useCallback(
    (event: LayoutChangeEvent) => {
      const { height } = event.nativeEvent.layout;

      if (height !== contentHeight) {
        setContentHeight(height);

        if (expanded) {
          animatedHeight.value = height;
        }
      }
    },
    [contentHeight, expanded, animatedHeight],
  );

  return (
    <Animated.View
      style={[{ width: '100%', overflow: 'hidden' }, animatedStyle]}
    >
      <View
        onLayout={handleLayout}
        style={{ position: 'absolute', width: '100%' }}
      >
        {children}
      </View>
    </Animated.View>
  );
}

Steps to reproduce

Nested screen with animated components. For example :

import {
  useState,
  useRef,
  useEffect,
  forwardRef,
  useImperativeHandle,
  ReactElement,
  useCallback,
} from 'react';
import {
  View,
  TextInput,
  Text,
  TextProps,
  TextInputProps,
  TextStyle,
  ViewStyle,
  TouchableWithoutFeedback,
  LayoutChangeEvent,
  StyleSheet,
} from 'react-native';
import Animated, {
  useAnimatedStyle,
  withTiming,
  useDerivedValue,
  interpolateColor,
  useSharedValue,
} from 'react-native-reanimated';

export interface InputProps extends TextInputProps {
  /** Style to the container */
  containerStyles?: ViewStyle;
  /** Show a preview of the input to the user */
  hint?: string;
  /** Set the color to the hint */
  hintTextColor?: string;
  /** Value for the label, same as placeholder */
  label: string;
  /** Callback for action submit on the keyboard */
  onSubmit?: () => void;
  /** Style to the input */
  inputStyles?: TextStyle;
  /** Required if onFocus or onBlur is overrided */
  isFocused?: boolean;
  /** Set a mask to the input. Example for dates: xx/xx/xxxx or phone +xx-xxx-xxx-xx-xx */
  mask?: string;
  /** Changes the input from single line input to multiline input */
  multiline?: true | false;
  /** Maximum number of characters allowed. Overridden by mask if present */
  maxLength?: number;
  /** Add left component to the input. Usually used for displaying icon */
  leftComponent?: ReactElement;
  /** Add right component to the input. Usually used for displaying icon */
  rightComponent?: ReactElement;
  /** Set custom animation duration. Default 200 ms */
  animationDuration?: number;
  /** Label Props */
  labelProps?: TextProps;
}

interface InputRef {
  focus(): void;
  blur(): void;
}

const AnimatedText = Animated.createAnimatedComponent(Text);

const INPUT_FONT_SIZE = 14;
const FOCUSED_LABEL_FONT_SIZE = 10;
const LABEL_TOP_PADDING = 6;

const Input = forwardRef<InputRef, InputProps>(
  (
    {
      label,
      labelProps,
      mask,
      maxLength,
      inputStyles,
      onChangeText,
      isFocused = false,
      onBlur,
      onFocus,
      leftComponent,
      rightComponent,
      hint,
      hintTextColor,
      onSubmit,
      containerStyles,
      multiline,
      value = '',
      animationDuration = 200,
      defaultValue,
      onPress,
      ...rest
    },
    ref,
  ) => {
    const [halfTop, setHalfTop] = useState<number>(0);
    const [isFocusedState, setIsFocusedState] = useState<boolean>(isFocused);
    const inputRef = useRef<TextInput>(null);

    const sharedValueOpacity = useSharedValue(value ? 1 : 0);
    const fontSizeAnimated = useSharedValue(
      isFocused ? FOCUSED_LABEL_FONT_SIZE : INPUT_FONT_SIZE,
    );
    const topAnimated = useSharedValue(0);
    const fontColorAnimated = useSharedValue(0);

    const handleFocus = () => setIsFocusedState(true);
    const handleBlur = () => !value && setIsFocusedState(false);

    const animateFocus = useCallback(() => {
      fontSizeAnimated.value = FOCUSED_LABEL_FONT_SIZE;
      topAnimated.value = defaultValue
        ? -(LABEL_TOP_PADDING * 2)
        : -halfTop + FOCUSED_LABEL_FONT_SIZE;
      fontColorAnimated.value = 1;
    }, [
      defaultValue,
      fontColorAnimated,
      fontSizeAnimated,
      halfTop,
      topAnimated,
    ]);

    const animateBlur = useCallback(() => {
      fontSizeAnimated.value = INPUT_FONT_SIZE;
      topAnimated.value = 0;
      fontColorAnimated.value = 0;
    }, [fontColorAnimated, fontSizeAnimated, topAnimated]);

    const onSubmitEditing = useCallback(() => {
      onSubmit?.();
    }, [onSubmit]);

    const style: TextStyle = StyleSheet.flatten([
      {
        alignSelf: 'center',
        position: 'absolute',
        flex: 1,
        zIndex: 999,
      },
    ]);

    const onChangeTextCallback = useCallback(
      (val: string) => {
        if (onChangeText) {
          onChangeText(val);
        }
      },
      [onChangeText],
    );

    const onLayout = (event: LayoutChangeEvent) => {
      const { height } = event.nativeEvent.layout;
      setHalfTop(height / 2 - LABEL_TOP_PADDING);
    };

    const positionAnimations = useAnimatedStyle(() => ({
      transform: [
        {
          translateY: withTiming(topAnimated.value, {
            duration: animationDuration,
          }),
        },
      ],
      opacity: withTiming(sharedValueOpacity.value, {
        duration: animationDuration,
      }),
      fontSize: withTiming(fontSizeAnimated.value, {
        duration: animationDuration,
      }),
    }));

    const progress = useDerivedValue(() =>
      withTiming(fontColorAnimated.value, { duration: animationDuration }),
    );

    const fontFamilyAnimated = useDerivedValue(() =>
      progress.value ? 'regular' : 'bold',
    );

    const colorAnimation = useAnimatedStyle(() => ({
      color: interpolateColor(progress.value, [0, 1], ['black', 'green']),
    }));

    const fontFamilyStyle = useAnimatedStyle(() => ({
      fontFamily: fontFamilyAnimated.value,
    }));

    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      blur: () => inputRef.current?.blur(),
    }));

    useEffect(() => {
      sharedValueOpacity.value = 1;
    }, [sharedValueOpacity]);

    useEffect(() => {
      if (isFocusedState || value) {
        animateFocus();
      } else {
        animateBlur();
      }
    }, [isFocusedState, value, animateFocus, animateBlur]);

    return (
      <View style={styles.container}>
        <TouchableWithoutFeedback
          onLayout={onLayout}
          onPress={(e) => {
            if (onPress) {
              onPress(e);
            }
            inputRef.current?.focus();
          }}
          style={{ flex: 1 }}
        >
          <View style={{ flexDirection: 'row', flexGrow: 1 }}>
            <View style={[styles.innerContainer, containerStyles]}>
              {leftComponent && leftComponent}
              <View style={{ flex: 1, flexDirection: 'row' }}>
                <AnimatedText
                  {...labelProps}
                  onPress={(e) => {
                    if (onPress) {
                      onPress(e);
                    }
                    inputRef.current?.focus();
                  }}
                  style={[
                    style,
                    { opacity: 0 },
                    positionAnimations,
                    colorAnimation,
                    fontFamilyStyle,
                    labelProps?.style,
                  ]}
                  suppressHighlighting
                >
                  {label}
                </AnimatedText>
                <TextInput
                  ref={inputRef}
                  onBlur={onBlur !== undefined ? onBlur : handleBlur}
                  onFocus={onFocus !== undefined ? onFocus : handleFocus}
                  onPress={onPress}
                  onSubmitEditing={onSubmitEditing}
                  value={value}
                  {...rest}
                  maxLength={mask?.length ?? maxLength}
                  multiline={multiline}
                  onChangeText={onChangeTextCallback}
                  placeholder={isFocusedState && hint ? hint : ''}
                  placeholderTextColor={hintTextColor}
                  style={StyleSheet.flatten([
                    inputStyles,
                    styles.input,
                    { top: LABEL_TOP_PADDING },
                  ])}
                />
              </View>
              {rightComponent && rightComponent}
            </View>
          </View>
        </TouchableWithoutFeedback>
      </View>
    );
  },
);

Input.displayName = 'Input';

export default Input;

const styles = StyleSheet.create({
  container: {
    gap: 8,
  },
  innerContainer: {
    flex: 1,
    flexDirection: 'row',
    borderWidth: 1,
    paddingVertical: 10,
    paddingHorizontal: 12,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'white',
  },
  input: {
    flex: 1,
    padding: 0,
    zIndex: 10,
    minHeight: 36,
    color: 'gray',
    fontWeight: 'bold',
  },
});

Snack or a link to a repository

Reanimated version

3.16.7

React Native version

0.75, 0.76. 0.77

Platforms

iOS, Android

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Fabric (New Architecture)

Build type

Debug app & dev bundle

Device

iOS simulator

Device model

No response

Acknowledgements

Yes

@github-actions github-actions bot added Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS labels Jan 31, 2025
Copy link

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?

Copy link

github-actions bot commented Jan 31, 2025

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 github-actions bot added Missing repro This issue need minimum repro scenario Missing info The user didn't precise the problem enough labels Jan 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Missing info The user didn't precise the problem enough Missing repro This issue need minimum repro scenario Platform: Android This issue is specific to Android Platform: iOS This issue is specific to iOS
Projects
None yet
Development

No branches or pull requests

1 participant