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 react-native-screens@3.20
and both these projects are experiencing the same broken behavior, expect it only happens in FlatList
s 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:
- 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.
- 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:
- Set
android:windowSoftInputMode="adjustNothing"
- Listen to keyboard hight changes
- Measure the View and its location to determine the needed diff/padding from the keyboard.
- 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.