Cristian
Cristian

Reputation: 291

React Native TextInput closes automatically when opened on android

I switched laptops and cloned the repository of my project, did a quick yarn install and looks like it was a big difference from the one on the main branch but I didn't bother since maybe it's just because of different Node versions.

Now every time I click on one TextInput the keyboard opens and closes immediately only on android. I attached a quick recording here. Tried some solutions and it looks like switching android:windowSoftInputMode from adjustResize to adjustPan in AndroidManifest.xml fixes the problem with the closing but I'm not really happy with the behaviour of the keyboard in the app when it's set to adjustPan. Maybe this issue starting happening a while ago but I just saw it now.

Here is just an input centered inside a simple View. https://gfycat.com/ordinaryquestionabledinosaur

Any suggestions anyone?

Upvotes: 17

Views: 13364

Answers (9)

German M
German M

Reputation: 11

I updated react-native from 0.64.3 to 0.66.5 and it worked like a charm. Maybe it also works with some version of 0.65.x . I'm also using react react-native-screens.

Upvotes: 1

Samer Murad
Samer Murad

Reputation: 2756

TL;DR

Scroll down for the solution

Explanation

Okay, so, its been a while since this issue was active, and honestly I find the answers here to be kind of half baked, I have a solution that fixes this problem regardless of react-native-screens, but the explanation is quite long, so bear with me here.

The solutions suggesting to upgrade the react-native-screens lib are no longer relevant, I am currently running two projects using [email protected] and both these projects are experiencing the same broken behavior, expect it only happens in FlatLists with inputs that are positioned on the lower part of the screen. So the Upper to middle inputs work fine, but all the inputs that would be placed under the keyboard once it opens, cause the above mentioned weird behavior. The solutions that suggest to change the windowSoftInputMode to "stateAlwaysHidden|adjustPan", i.e: android:windowSoftInputMode="stateAlwaysHidden|adjustPan". Fail to mention that this changes the behavior of how the screen resize works entirely, and disallow users to scroll beyond the initially clicked TextInput, so if u have a list with multiple text fields, this solution isn't gonna work for you (It didn't for me and my use case).

The solution I'm suggesting is similar to stateAlwaysHidden|adjustPan expect it uses android:windowSoftInputMode="adjustNothing" instead. In essence, what this will do, is tell android to do nothing when the windowSoftInput (i.e: the software keyboard) opens up. essentially eliminating the automatic layout adjustments. Which achieves two important things:

  1. It aligns the resizes behavior (or lack there of) to the behavior of iOS, which is that the view layout stays the same, under the keyboard.
  2. Allows us to do our own custom logic, which can now be the same for both iOS and Android, because as mentioned in 1., both platforms should now behave the same when the keyboard opens up.

The solution itself, will be to listen to keyboard height changes (open/close) events, and to adjust a padding to the wrapping View container, so it essentially "dodges" the keyboard, leaving the scrollview/listView/flatlist/etc (if present) above the keyboard, making it look seamless. This however, requires that we know the size of the view that needs to be padded, so we can extract the diff that needs to be padded (in case the View in question isn't the last element on the screen; incase you have bottom action buttons for example). All of this can then be packed in a newly composed View, we will called it KeyboardPaddedView (not to confuse with the built-in KeyboardAvoidingView, which I find to be garbage btw). This view will contain all the logic, and can then just be used instead of the original View, which wraps the Flatlist/ScrollView etc.

So again, summed up:

  1. Set android:windowSoftInputMode="adjustNothing"
  2. Listen to keyboard hight changes
  3. Measure the View and its location to determine the needed diff/padding from the keyboard.
  4. Wrap everything inside the Measured view for reusability

The solution

<!-- 1. Set `android:windowSoftInputMode="adjustNothing"` -->
<!-- AndroidManifest.xml -->
...
<activity
          ...
            android:windowSoftInputMode="adjustNothing"
          ...
>
...

</activity>

// keyboard.hks.ts

// 2. Listen to keyboard hight changes (written in typescript)

// Keyboard event hook
export const useOnKeyboardEvent = (
  eventName: KeyboardEventName,
  onEvent: (event: KeyboardEvent) => void,
) => {
  const cb = useCallback((event: KeyboardEvent) => onEvent(event), [onEvent]);
  useEffect(() => {
    const listener = Keyboard.addListener(eventName, cb);
    return () => listener.remove();
  }, [eventName, cb]);
};



type OnUpdateCB = (newHeight: number) => void;
// hook to calculate and set keyboard height
export const useOnKeyboardHeightUpdate = (onUpdate: OnUpdateCB) => {
  const keyboardOpenEvent = useMemo(
    () => (Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'),
    [],
  );
  const keyboardCloseEvent = useMemo(
    () => (Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'),
    [],
  );

  const cb = useCallback((height: number) => onUpdate(height), [onUpdate]);
  useOnKeyboardEvent(keyboardOpenEvent, (event) =>
    cb(event.endCoordinates.height),
  );
  useOnKeyboardEvent(keyboardCloseEvent, () => cb(0));
};

// measures.hks.ts
// 3. Measure the View and its location to determine the needed diff/padding from the keyboard.


// hook to Measure a view's layout inside the container its in,
// in relation to the window, i.e its global/absolute position on screen

export const useWindowMeasuredViewRef = (): [
  MutableRefObject<View | undefined>,
  LayoutRectangle,
  (el: View) => void,
] => {
  const viewRef = useRef<View>();
  const [layoutRect, setLayoutRect] = useState({ x: 0, y: 0, width: 0, height: 0 });
  const callbackSetter = useCallback(async (el: View) => {
    async function nextFrameAsync() {
      return new Promise<void>((resolve) =>
        requestAnimationFrame(() => resolve()),
      );
    }
    // set viewRef element
    viewRef.current = el;
    // view measurements will only be available on the next frame, so we simply await the next frame,
    // so we can correctly measure the view and its layout
    await nextFrameAsync();
    viewRef.current?.measureInWindow(
      (x: number, y: number, width: number, height: number) => {
        setLayoutRect({
          x,
          y,
          height,
          width,
        });
      },
    );
  }, []);

  return [viewRef, layoutRect, callbackSetter];
};


// Using the measured view
// get needed bottom padding for keyboard height to offset FlatList component using bottom padding
// helps to get the view above the keyboard correctly, regardless of any view beneath it
export function useKeyboardPaddedHeightForView(): [number, (el: View) => void] {
  const [_viewRef, layout, viewRefSetter] = useWindowMeasuredViewRef();
  const windowDimensions = useWindowDimensions();
  const [keyboardHeight, _setKeyboardHeight] = useState(0);
  const diff = useMemo(
    () => windowDimensions.height - layout.height - Math.floor(layout.y),
    [windowDimensions.height, layout],
  );
  const setKeyboardHeight = useCallback(
    (height: number) => {
        _setKeyboardHeight(height - diff);
    },
    [_setKeyboardHeight, diff],
  );

  // use the above implemented keyboard calculation
  useOnKeyboardHeightUpdate(setKeyboardHeight);

  return [keyboardHeight, viewRefSetter];
}

// KeyboardPaddedView.tsx
// 4. Wrap everything inside the Measured view for reusability
type Props = ViewProps & {};

function KeyboardPaddedView(props: Props): ReactElement {
  const { children, style, ...rest } = props;
  const [keyboardHeight, viewRefSetter] = useKeyboardPaddedHeightForView();
  const keyboardHeightStyle = {
    paddingBottom: keyboardHeight > 0 ? keyboardHeight : 0,
  };
  return (
    <View ref={viewRefSetter} style={[style, keyboardHeightStyle]} {...rest}>
      {children}
    </View>
  );
}

export default KeyboardPaddedView;

That's it, the KeyboardPaddedView can now be used accordingly, to avoid the keyboard, on both iOS and Android, which will now have the same behavior.

Honestly at this point I don't think anyone will read this, but just in case anyone does, sorry for the long solution, if anyone actually ends up reading and/or using this, hope this helps.

Upvotes: 3

amboji alur
amboji alur

Reputation: 427

Replace the line in the AndroidManifest.xml file:

Before

 android:windowSoftInputMode="adjustResize"

After

 android:windowSoftInputMode="stateAlwaysHidden|adjustPan"

Upvotes: 0

Bruno Diaz
Bruno Diaz

Reputation: 99

Change the line in AndroidManifest.xml works for me, I did the update but that does not works

Change the config to:

android:windowSoftInputMode="stateAlwaysHidden|adjustPan"

Upvotes: 3

reggi49
reggi49

Reputation: 560

if you're using react native version "0.64.*". Just set react-native-screens version to "3.4.0". Do not use "^" to avoid npm updated it. and it worked for me.

Upvotes: 0

Virgil Nauleau
Virgil Nauleau

Reputation: 139

For those who have the same problem, having both botton and top padding often result on the keyboard closing itself when opening. Took a while to discover that

Upvotes: 0

Bilal Yaqoob
Bilal Yaqoob

Reputation: 1007

Change the line in AndroidManifest.xml

Old line android:windowSoftInputMode="adjustResize"

New line android:windowSoftInputMode="stateAlwaysHidden|adjustPan"

Upvotes: 18

Jaweria Siddiqui
Jaweria Siddiqui

Reputation: 11

Just Upgraded react-native-screen to latest version.

yarn add react-native-screens

"react-native-screens": "^3.13.1"

Upvotes: 1

Biswanath Tewari
Biswanath Tewari

Reputation: 355

Was facing a similar issue, turned out that the react-native-screens library was causing the problem. Try setting the version to "~3.10.2". Worked out for me.

Upvotes: 34

Related Questions