kirillzyusko / react-native-keyboard-controller

Keyboard manager which works in identical way on both iOS and Android
https://kirillzyusko.github.io/react-native-keyboard-controller/
MIT License
1.38k stars 55 forks source link

KeyboardAvoidingView sibling to KeyboardAwareScrollView creates empty space at the bottom of the KeyboardAwareScrollView #451

Closed itsramiel closed 1 month ago

itsramiel commented 1 month ago

Describe the bug When KeyboardAvoidingView is a sibling to KeyboardAwareScrollView then:

  1. KeyboardAwareScrollView has extra space at the bottom of it (main issue)
  2. The textinput inside KeyboardAwareScrollView is not correctly positioned from the first try, take a bit to adjust

Code snippet What I am trying to achieve is a screen with a list that contains arbitrary views and an input and a footer button. I would like when the user click on the input for it to be focused/visible while also having the button visible to allow the user to take action(done button in footer) without needing to close the keyboard first

import {Button, SafeAreaView, TextInput, View} from 'react-native';
import {
  KeyboardAvoidingView,
  KeyboardAwareScrollView,
  KeyboardProvider,
} from 'react-native-keyboard-controller';

function App() {
  return (
    <KeyboardProvider>
      <Screen />
    </KeyboardProvider>
  );
}

function Screen() {
  return (
    <SafeAreaView style={{flex: 1}}>
      <KeyboardAwareScrollView contentContainerStyle={{gap: 8}}>
        {Array(7)
          .fill(0)
          .map((_, i) => (
            <View key={i} style={{height: 80, backgroundColor: 'blue'}} />
          ))}
        <TextInput style={{padding: 16, backgroundColor: 'red'}} />
      </KeyboardAwareScrollView>
      <KeyboardAvoidingView behavior="padding">
        <Button title="done" />
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
}

export default App;

Repo for reproducing https://github.com/itsramiel/keyboard-controller-repro

To Reproduce Steps to reproduce the behavior:

  1. clone the repo
  2. click on the red text input, then scroll down

Expected behavior I expect not to have extra space on the bottom

ScreenRecording

Smartphone (please complete the following information):

Additional context I am not sure why the text input also lags a bit behind and is not correctly position from the first time.

kirillzyusko commented 1 month ago

@itsramiel shouldn't you use KeyboardStickyView instead of KeyboardAvoidingView in this example?

From a quick look KeybiardStickyView should fit better in this layout. I have an example app for a reference: https://github.com/kirillzyusko/react-native-keyboard-controller/blob/main/example/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx

itsramiel commented 1 month ago

I don't think KeyboardStickyView is better here since it overlays other views and in your example you are creating a state variable for the height of the footer and using that for the offset of the KeyboardAwareScrollView. I'd rather it be more declarative and. fail proof.

Actually in my case I want the whole screen to be avoiding the keyboard along with the scrollview to keep the focused input visible so I can change my returned jsx to:

  return (
    <KeyboardAvoidingView style={{ flex: 1 }} behavior="padding">
      <KeyboardAwareScrollView
        style={{ flex: 1 }}
        contentContainerStyle={{ gap: 8 }}>
        {Array(7)
          .fill(0)
          .map((_, i) => (
            <View key={i} style={{ height: 80, backgroundColor: 'blue' }} />
          ))}
        <TextInput style={{ padding: 16, backgroundColor: 'red' }} />
      </KeyboardAwareScrollView>
      <Button title="done" />
    </KeyboardAvoidingView>
  );

but I still get the same behavior:

https://github.com/kirillzyusko/react-native-keyboard-controller/assets/80689446/8c64dd1c-8b20-44eb-99ea-5099cb9674be

changes are on branch alt1

One thing that I tried by mistake and kinda works is to wrap the whole screen with KeyboardAvoidingView like above but also to replace KeyboardAvoidingScrollView with normal scrollview then the extra space is no longer there but it takes a while for the focused input to come into view. I dont really understand why this kinda works. I really tried that by mistake and can the input be focused immediately without a delay?

code:

  return (
    <KeyboardAvoidingView style={{flex: 1}} behavior="padding">
      <ScrollView style={{flex: 1}} contentContainerStyle={{gap: 8}}>
        {Array(7)
          .fill(0)
          .map((_, i) => (
            <View key={i} style={{height: 80, backgroundColor: 'blue'}} />
          ))}
        <TextInput style={{padding: 16, backgroundColor: 'red'}} />
      </ScrollView>
      <Button title="done" />
    </KeyboardAvoidingView>
  );

video:

changes are on branch alt2

Looking forward to you reply!

kirillzyusko commented 1 month ago

Hey @itsramiel

Sorry for a long response. Basically when you combine two view (i. e. KeyboardAvoidingView and KeyboardAwareScrollView) they both will add its own padding - and it eventually will lead to double space -> this is how it's designed to work.

Regarding wrapping ScrollView into KeyboardAvoidingView - it may work, but honestly I don't know why scroll happens automatically. Maybe some code calls scrollResponderScrollNativeHandleToKeyboard (from ScrollView) - need to investigate it. But since it's handled mostly on RN core - you will not get frame in frame precision (i. e. the delay will be always present).

My suggestion would be to stick to example app and use KeyboardAwareScrollView + KeyboardStickyView.

and in your example you are creating a state variable for the height of the footer and using that for the offset of the KeyboardAwareScrollView. I'd rather it be more declarative

You always can create your own component, like:

const KeyboardAwareScrollViewWithFooter = ({ footer, children }) => {
  const [footerOffset, setFooterOffset] = useState(0);

  const onLayout = (e) => {
    setFooterOffset(e.nativeEvent.layout.height);
  };

  return (
    <>
      <KeyboardAwareScrollView>
        {children}
      </KeyboardAwareScrollView>
      <View onLayout={onLayout}>
        {footer}
      </View>
    </>
  );
}

And use this component in declarative way. Let me know if I'm missing anything 🙏

itsramiel commented 1 month ago

I would have preferred to have a solution without manually measuring the height and applying a bottom padding but if that is the state then alright. Thanks for the response. I will be closing the issue