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

KeyboardAwareScrollView freezes another animation on the screen when opening the keyboard #750

Closed
denysoleksiienko opened this issue Jan 2, 2025 · 20 comments
Assignees
Labels
🍎 iOS iOS specific KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component

Comments

@denysoleksiienko
Copy link

denysoleksiienko commented Jan 2, 2025

Describe the bug
I am using an input with a floating label implemented with react-native-reanimated. When I use the native ScrollView, the animations remain smooth when the keyboard opens. However, when I use KeyboardAwareScrollView, my labels sometimes experience delays. I will add the code for my input and a video demonstrating the different behaviors. Also works well with KeyboardAvoidingView.

Code snippet

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',
  },
});

Expected behavior
Keyboard in KeyboardAwareScrollView shouldn't freeze another animation on the screens

Screenshots

KeyboardAwareScrollView

Screen.Recording.2025-01-02.at.18.21.32.mov

Native ScrollView

Screen.Recording.2025-01-02.at.18.22.06.mov

Smartphone (please complete the following information):

  • Desktop OS: MacOS
  • Device: Simulator 16 Pro / iPhone 14 Pro Max
  • OS: 18+
  • RN version: 0.76.5
  • RN architecture: new arc
  • JS engine: [e.g. JSC, Hermes, v8]
  • Library version: 1.15.2

Additional context

"@react-navigation/native": "^7.0.14",
"@react-navigation/stack": "^7.1.1",
"react-native-gesture-handler": "^2.21.2",
"react-native-reanimated": "^3.15.1",
"react-native-safe-area-context": "^5.0.0",
"react-native-screens": "^4.4.0"
@kirillzyusko kirillzyusko added 🍎 iOS iOS specific KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component labels Jan 2, 2025
@kirillzyusko
Copy link
Owner

@denysoleksiienko is it iOS only? Android works well?

@kirillzyusko
Copy link
Owner

Have you tried to animate it as fontColorAnimated.value = withTiming(1, { duration: animationDuration })? I. e. to skip a pipeline with useDeriverValue?

I suspect that this code:

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

Can be triggered every time when keyboard changes its frame, and you restart animation every time (it's only assumption).

@denysoleksiienko
Copy link
Author

It looks like the problem is in the react-native-reanimated version. I will test more

@denysoleksiienko
Copy link
Author

I updated react-native-reanimated to the latest version 3.16.6 and it seems to help in my case with animated input labels, but KeyboardAwareScrollView is not smooth scrolling when the keyboard is opening on ios/android

@kirillzyusko
Copy link
Owner

but KeyboardAwareScrollView is not smooth scrolling when the keyboard is opening on ios/android

Can you post a video, please? In my example project it's pretty smooth:

Simulator.Screen.Recording.-.iPhone.15.Pro.-.2025-01-07.at.13.23.34.mp4

@CaptainJeff
Copy link

Finally tracked this issue to this dependency. I even noticed that using KeyboardAvoidingView at some point will mess up my animations for my react navigation Drawer Nav and eventually crash the app. Seems to mess up almost all of my animations related to react-native-reanimation.

I'm pretty sure its linked ot this warning as well

[Reanimated] Tried to modify key value of an object which has been already passed to a worklet. See
https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-shareable
for more details.

@CaptainJeff
Copy link

@denysoleksiienko were you able to figure out anything else related to this issue?

@denysoleksiienko
Copy link
Author

@CaptainJeff
With these versions:

"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/stack": "^7.1.1",
"react-native": "0.75.2",   
"react-native-gesture-handler": "^2.18.1",
"react-native-reanimated": "^3.15.1",
"react-native-safe-area-context": "^4.10.9",
"react-native-screens": "^4.4.0",
"react-native-keyboard-controller": "^1.15.2"

So it works exactly the same as in the video from the first message with freezes.


However, regarding the new version RN + Reanimated, it’s hard for me to determine whether the issue is with our optimization or if there’s something wrong with Reanimated or another library. But after upgrading RN to 0.76.x and Reanimated to 3.16.x all my animations working with freezes which works on 0.75 and 3.15 well.

"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/stack": "^7.1.1",
"react-native": "0.76.6", 
"react-native-gesture-handler": "^2.22.0",
"react-native-reanimated": "^3.16.7",
"react-native-safe-area-context": "^5.1.0",
"react-native-screens": "^4.4.0",
"react-native-keyboard-controller": "^1.15.2"

I noticed that if we use animations at the bottom root, they are smoother, but if we navigate to another navigator and go to the 3rd or 4th screen (where our form is located), the animations become less smooth.

It’s difficult to figure out, because we’re using the nx monorepo.
Here are two videos: one from the bottom root and one from the other navigator. You can see how a little freeze at the bottom screen when focused on the latest inputs

bottom root next navigator 3rd screen
iPhone.16.Pro.-.bootm_root.mp4
iPhone.16.Pro.-.nested_nav.mp4

PS:
Looks like the better combination is "react-native": "0.75.x" and latest "react-native-reanimated", but reanimated doesn't work on Android, need to upd RN to 0.76.x

@CaptainJeff
Copy link

@denysoleksiienko interesting. Thanks for the update!

I realized like 30 minutes ago that my issue is probably something different. Once any invocation of react-native-keyboard-controller was displayed nearly all subsequent reanimation attempts would lag and eventually crash my app. Even stuff like togglign the react navigation drawer. But mine was caused by having

"animation: none" on a parent Navigator (react navigation). Once i removed that I fixed the crashing and most of the animation issues. I'm still getting some lag but I don't think thats related to this library anymore.

I suspect your issue is different but wanted to share that incase anyone else comes across it

@kirillzyusko
Copy link
Owner

@denysoleksiienko maybe you can prepare a demo with nested screens? If it's reproducible in isolated project then I can help to debug the problem 😊

@CaptainJeff
Copy link

@denysoleksiienko sorry. I was just wanted to clarify something. I was wrong when I said it was only the animation styles causing my issue. That fixed a different issue I was having with react reanimation.

My initial assertion was correct. This library caused reanimation issues. IE

When i used KeyboardAvoidingView and imported it from react-native-keyboard-controller I couldn't use subsequent animations like getting my Drawer Navigator to toggle. When i changed teh import from react-native-keyboard-controller to react-native the issue went away.

Seems like its related to this error [Reanimated] Reading from valueduring component render. Please ensure that you do not access thevalueproperty or useget method of a shared value while React is rendering a component

@kirillzyusko
Copy link
Owner

@CaptainJeff may I kindly ask you to provide a minimal reproduction example?

[Reanimated] Reading from value during component render. Please ensure that you do not access the value property or use get method of a shared value while React is rendering a component

I strongly believe it has been fixed in #662 Which react-native-keyboard-controller version do you use? In my example project I don't see such warnings anymore 🤔
The fix was published in 1.14.3 version

@denysoleksiienko
Copy link
Author

@kirillzyusko It seems like when I remove the AnimatedText label the scrolling becomes smoother but it doesn't look enough. I will try to prepare this screen from my project

@kirillzyusko
Copy link
Owner

I will try to prepare this screen from my project

Awesome, thank you ❤

@denysoleksiienko
Copy link
Author

@kirillzyusko Hello! Here, I quickly made a user form screen that has lags and freezes, although, on the previous version of react-native-reanimated 3.15.1, it works normally. I thought this was a problem with our nx workspace, but it's reproducible on the clean project
https://github.com/denysoleksiienko/keyboard-controller/tree/main

For some reason, the collapsible card and bottom sheet are freezes/lags. Additionally, when using KeyboardAwareScrollView, there are some lags at the end scroll. I also noticed that if the last focus was on the last promo input, and then you scroll up the screen and switch between full and child ButtonGroup or expand the collapsible data, the scroll jumps to the end. Please help

push up screen expand card bottom sheet
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-09.at.16.05.08.mp4
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-09.at.16.05.34.mp4
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-02-09.at.16.05.50.mp4

@kirillzyusko
Copy link
Owner

@denysoleksiienko okay, checking it now 👀

@denysoleksiienko
Copy link
Author

@kirillzyusko any update? 🙂

@denysoleksiienko
Copy link
Author

Freezes and issues in Reanimated 3.16.x in New Arch

@kirillzyusko
Copy link
Owner

@denysoleksiienko may I kindly ask you to send here a link to the issue in the reanimated repository? 👀

@denysoleksiienko
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍎 iOS iOS specific KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component
Projects
None yet
Development

No branches or pull requests

3 participants