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

Smooth Keyboard Animation Height Transition in useKeyboardHandler #677

Open
lucaspelloni2 opened this issue Nov 8, 2024 · 11 comments
Open
Assignees
Labels
🍎 iOS iOS specific question You wanted to clarify something about the usage of the library or have a question about something

Comments

@lucaspelloni2
Copy link

lucaspelloni2 commented Nov 8, 2024

Describe the bug
Hello! Thank you for this library. Not sure if this is intended, but when animating the keyboard, the height value updates with noticeable “steps” between values, causing height animations to appear choppy rather than smooth. This “jumping” behavior detracts from the expected fluidity in the keyboard’s animation.

Code snippet

export const useKeyboardAnimation = () => {
  const progress = useSharedValue(0);
  const height = useSharedValue(0);

  useKeyboardHandler({
    onStart: e => {
      'worklet';
      progress.value = e.progress;
    },
    onMove: e => {
      'worklet';
      progress.value = e.progress;
      height.value = e.height;
    },
    onEnd: e => {
      'worklet';
      height.value = e.height;
      progress.value = e.progress;
    },
  });

  const style = useAnimatedStyle(() => {
    console.log('current height: ', height.value);
    return {
      height: height.value,
    };
  }, [height]);

  return useMemo(() => ({ height }), [height]);
};

LOG current height: 291
LOG current height: 249.05628901209576
LOG current height: 217.09624960612547
LOG current height: 184.88664681262821
LOG current height: 154.56893747673666
LOG current height: 127.46685444750403
LOG current height: 103.68985801017402
etc.

To Reproduce
Run the code above

Expected behavior
Ideally, the height value should decrease smoothly without noticeable steps, creating a fluid animation when the keyboard is animated (and so access each frame)

Screenshots
Not needed

Smartphone (please complete the following information):

  • Device: iPhone 12 Pro
  • RN version: 0.74.5
  • RN architecture: old
  • Library version: 1.13.4

Additional context
This issue seems to stem from the stepped updates in height.value, likely related to the timing of the onMove events or the underlying keyboard animation handler in Reanimated. Any guidance or alternative methods for achieving a smoother keyboard animation would be appreciated.

@lucaspelloni2 lucaspelloni2 changed the title Frame drops in useKeyboardHandler Smooth Keyboard Animation Height Transition in useKeyboardAnimation Nov 8, 2024
@lucaspelloni2 lucaspelloni2 changed the title Smooth Keyboard Animation Height Transition in useKeyboardAnimation Smooth Keyboard Animation Height Transition in useKeyboardHandler Nov 8, 2024
@kirillzyusko
Copy link
Owner

Hey @lucaspelloni2

May I ask you to attach a video to show the choppy animation?

Are you testing on a real device or simulator? Also may I ask you to provide a full code snippet?

Basically onMove should produce frame-in-frame values (especially on real device, on simulator sometimes it can be slightly out-of-date). And I'm asking because there could be other problems (maybe your layout when you change height require re-calculation and because of that animation looks choppy).

You can also clone example app and check screens like Animated keyboard/Chat flatlist/KeyboardAvoidingView/KeyboardAwareScrollView - I use onMove handler everywhere on these screens and animation is very smooth.

@kirillzyusko kirillzyusko added the 🍎 iOS iOS specific label Nov 8, 2024
@lucaspelloni2
Copy link
Author

Thank you for the response!!

Here my full code snippet

import {ChatInput} from '@/components/ui/ChatInput';
import {AnimatedFlex, PressableFlex} from '@/components/ui/core/Containers';
import {__COLORS} from '@/components/ui/theme';
import {useKeyboardAnimation} from '@/hooks/keyboard';
import {FlashList} from '@shopify/flash-list';
import {useState} from 'react';
import {TouchableOpacity} from 'react-native';
import {useAnimatedStyle} from 'react-native-reanimated';
import {MakeSpacing} from '../ui/MakeSpacing';
import {TopPadder} from '../ui/TopPadder';
import {Flex} from '../ui/core/Flex';
import {Regular} from '../ui/core/Text';

const colors = ['yellow', 'green', 'blue', 'red'];
const DummyListItem = ({index}: {index: number}) => {
  return (
    <PressableFlex height={200} width={200} background={colors[index]}>
      <TouchableOpacity>
        <Regular center color={__COLORS.background}>
          Dummy text.
        </Regular>
      </TouchableOpacity>
    </PressableFlex>
  );
};
export const AgentChat = () => {
  const [newMessage, setNewMessage] = useState('');
  const {height} = useKeyboardAnimation();

  const fakeViewStyle = useAnimatedStyle(() => {
    return {
      height: height.value,
    };
  }, [height]);

  const renderDummyItem = ({index}: {index: number}) => {
    return <DummyListItem index={index} />;
  };

  return (
    <AnimatedFlex flex={1} background={__COLORS.background}>
      <TopPadder />
      <MakeSpacing yMultiply={10} />
      <Flex flex={1} justify="center" align="center">
        <FlashList
          horizontal
          data={Array.from({length: colors.length}, (_, index) => index)}
          renderItem={renderDummyItem}
        />
      </Flex>
      <Flex column>
        <ChatInput
          autoFocus
          placeholder="Add more information..."
          selectionColor={__COLORS.white}
          placeholderTextColor={__COLORS.white}
          value={newMessage}
          onSend={() => console.log('send')}
          onChangeText={setNewMessage}
        />
        <MakeSpacing yMultiply={2} />
      </Flex>
      <AnimatedFlex style={fakeViewStyle} />
    </AnimatedFlex>
  );
};

@lucaspelloni2
Copy link
Author

lucaspelloni2 commented Nov 8, 2024

real device, iPhone 12 Pro

ScreenRecording_11-08-2024.11-15-38_1.MP4

@kirillzyusko
Copy link
Owner

@lucaspelloni2 Are you using Sentry by any chance? If yes, then which version?

@lucaspelloni2
Copy link
Author

Nope. no Sentry (we arent live yet)

@kirillzyusko
Copy link
Owner

Okay, this is something strange. I fixed onMove precision in #412

Now I'm testing a test case with translateY animation and it works frame in frame:

ScreenRecording_11-08-2024.11-32-35.AM_1.MP4

When I test Chat flatlist (the layout similar to yours), then I'm getting a delayed (not choppy) animation 😠:

ScreenRecording_11-08-2024.11-32-21.AM_1.MP4

Captured on iPhone 11, iOS 18

I definetely remember, that when I was testing #412 I tested Chat example as well with updated implementation of onMove and back to the times it was working like this:

RPReplay_Final1714507624.MP4

Captured on iOS 11, iOS 17.5

I didn't modify the code, but what has changes since April is:

  • new version of react-native;
  • new version of react-native-reanimated (which version do you use by the way?)
  • new version of iOS (I'm going to test iOS 15 today and post results here)

May I also ask yo to prepare your example so that I can copy/paste it in my example project and test as well? Right now some components (such as AnimatedFlex/MakeSpacing/Regular etc.) are not visible to me. Would be good if you could replace all these parts with simple View component, so that I can easily copy/paste and test on my device.

@lucaspelloni2
Copy link
Author

Thank you very much for this!

With this code you should be able to reproduce the issue

import {FlashList} from '@shopify/flash-list';
import {useCallback, useState} from 'react';
import {Text, TextInput, TouchableOpacity, View} from 'react-native';
import {useKeyboardHandler} from 'react-native-keyboard-controller';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

const colors = ['yellow', 'green', 'blue', 'red'];
const DummyListItem = ({index}: {index: number}) => {
  return (
    <TouchableOpacity
      style={{height: 200, width: 200, backgroundColor: colors[index]}}>
      <Text>Dummy text.</Text>
    </TouchableOpacity>
  );
};
export const AgentChat = () => {
  const [newMessage, setNewMessage] = useState('');
  const height = useSharedValue(0);

  useKeyboardHandler({
    onStart: e => {
      'worklet';
      height.value = Math.abs(e.height);
    },
    onMove: e => {
      'worklet';
      height.value = Math.abs(e.height);
    },
    onEnd: e => {
      'worklet';
      height.value = Math.abs(e.height);
    },
    onInteractive: e => {
      'worklet';
      height.value = Math.abs(e.height);
    },
  });

  const fakeViewStyle = useAnimatedStyle(() => {
    return {
      height: height.value + 20,
    };
  }, [height]);

  const renderDummyItem = useCallback(({index}: {index: number}) => {
    return <DummyListItem index={index} />;
  }, []);

  return (
    <Animated.View style={[{flex: 1, backgroundColor: 'black'}]}>
      <View style={{height: 200}} />
      <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
        <FlashList
          horizontal
          data={Array.from({length: colors.length}, (_, index) => index)}
          renderItem={renderDummyItem}
          estimatedItemSize={200}
        />
      </View>
      <TextInput
        autoFocus
        placeholder="Add more information..."
        selectionColor={'white'}
        placeholderTextColor={'white'}
        value={newMessage}
        onChangeText={setNewMessage}
      />
      <Animated.View style={fakeViewStyle} />
      <View style={{height: 20}} />
    </Animated.View>
  );
};

@lucaspelloni2
Copy link
Author

Here again the video. In my case, the height value not only updates with a delay but also shows distinct steps between value updates. This stepping effect causes the keyboard animation to stutter, which reduces the smoothness.

ScreenRecording_11-08-2024.11-55-43_1.MP4

@lucaspelloni2
Copy link
Author

and btw:

    "react-native-keyboard-controller": "^1.13.4",
    "react-native": "0.74.5",
     "react-native-reanimated": "~3.15.3",
     "@shopify/flash-list": "1.6.4",
    

@kirillzyusko
Copy link
Owner

Awesome, I'll try to check that today or over the weekend 👀

@kirillzyusko
Copy link
Owner

Hey @lucaspelloni2

I'm slightly late 😅 I tried to reproduce a problem but on my iPhone 11 (iOS 18.0.1) the animation is running frame in frame:

ScreenRecording_11-11-2024.12-31-59.PM_1.MP4

Though I have to admit that I made some modifications to your code:

import { useCallback, useState } from "react";
import {
  FlatList as FlashList, // <-- modification (1)
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from "react-native";
import { useKeyboardHandler } from "react-native-keyboard-controller";
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from "react-native-reanimated";

const colors = ["yellow", "green", "blue", "red"];
const DummyListItem = ({ index }: { index: number }) => {
  return (
    <TouchableOpacity
      style={{ height: 200, width: 200, backgroundColor: colors[index] }}
    >
      <Text>Dummy text.</Text>
    </TouchableOpacity>
  );
};

export const AgentChat = () => {
  const [newMessage, setNewMessage] = useState("");
  const height = useSharedValue(0);

  useKeyboardHandler({
    onStart: (e) => {
      "worklet";
      // height.value = Math.abs(e.height); // <-- modification (2)
    },
    onMove: (e) => {
      "worklet";
      height.value = Math.abs(e.height);
    },
    onEnd: (e) => {
      "worklet";
      height.value = Math.abs(e.height);
    },
    onInteractive: (e) => {
      "worklet";
      height.value = Math.abs(e.height);
    },
  });

  const fakeViewStyle = useAnimatedStyle(() => {
    return {
      height: height.value + 20,
    };
  }, [height]);

  const renderDummyItem = useCallback(({ index }: { index: number }) => {
    return <DummyListItem index={index} />;
  }, []);

  return (
    <Animated.View style={[{ flex: 1, backgroundColor: "black" }]}>
      <View style={{ height: 200 }} />
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <FlashList
          horizontal
          data={Array.from({ length: colors.length }, (_, index) => index)}
          estimatedItemSize={200}
          renderItem={renderDummyItem}
        />
      </View>
      <TextInput
        autoFocus
        placeholder="Add more information..."
        placeholderTextColor={"white"}
        selectionColor={"white"}
        value={newMessage}
        onChangeText={setNewMessage}
      />
      <Animated.View style={fakeViewStyle} />
      <View style={{ height: 20 }} />
    </Animated.View>
  );
};


export default AgentChat; // <-- modification (3)

Most noticeable are:

  • I use FlatList instead of FlashList (but I don't think it does make a difference in this particular example). You can also try to use FlatList and let me know if it changes anything for you.
  • I removed height.value = Math.abs(e.height); code in onStart. onStart has always a "destination" value, so if you update it immediately you may encounter problems, like immediate jumps etc. (especially on Android). In your case such jumps happens on your video here:
image

On iOS you may do this (i. e. update value immediately) and iOS will schedule a layout animation and perform smooth transitions. But if you use both onStart + onMove, then you may get a conflict, when iOS tries to animate everything via layout animation and you change position every frame, so you get stuttering effect.

Please, try to remove height modifications from onStart and let me know if the issue has been fixed for you or not 🙏

@kirillzyusko kirillzyusko added the question You wanted to clarify something about the usage of the library or have a question about something label Nov 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍎 iOS iOS specific question You wanted to clarify something about the usage of the library or have a question about something
Projects
None yet
Development

No branches or pull requests

2 participants