Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Android][Keyboard] More consistent inequality check to compute keybo…
…ard state (#5874) <!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please follow the template so that the reviewers can easily understand what the code changes affect. --> ## Summary I have found the `useAnimatedKeyboard` hook to be particularly useful. Beyond the height value, I often use the state value too, which allows me to show or hide elements based on the keyboard's state. In [this example](https://github.com/antFrancon/reanimated-keyboard-state), I combine it with a regular React Native `KeyboardAvoidingView` to display a toolbar above my keyboard. The issue I encountered is that the computed Android keyboard state becomes invalid after opening and then closing the keyboard for the first time. After some investigation, it appears that the keyboard gets a negative height upon closing, which disrupts some logic inside `keyboard.java`: ```ts // onAnimationStart mState = mHeight == 0 ? KeyboardState.OPENING : KeyboardState.CLOSING // onAnimationEnd mState = mHeight == 0 ? KeyboardState.CLOSED : KeyboardState.OPEN; ``` As a result, the keyboard state cycle appears to be `0 (UNKNOWN) > 1 (OPENING) > 2 (OPEN) > 3 (CLOSING) > 2 (OPEN)` instead of the expected `0 (UNKNOWN) > 1 (OPENING) > 2 (OPEN) > 3 (CLOSING) > 4 (CLOSED)`. | Ios | Android | | -------- | ------- | | ![ios-keyboard](https://github.com/software-mansion/react-native-reanimated/assets/72255317/2115900f-3655-4204-a84a-64499a2869b1) | ![android-keyboard](https://github.com/software-mansion/react-native-reanimated/assets/72255317/6f1f0571-5845-472e-966a-09c3d586d354) | There may be an issue with the fact that the height gets a negative value, but this PR does not intend to address it. Instead, I would like to make the code more robust regarding keyboard state computation by using an inequality check, specifically `mHeight <= 0`, instead of a strict equality check `mHeight == 0`. ## Test plan I have added a [ready-to-play repository](https://github.com/antFrancon/reanimated-keyboard-state). It essentially implements the code described above. I've added some logs to illustrate that the keyboard state is incorrect on Android when opening and closing the keyboard. You can also observe that the keyboard gets a negative height after closing. ```ts import React from 'react'; import { KeyboardAvoidingView, ScrollView, StatusBar, StyleSheet, Text, TextInput, View, useColorScheme, } from 'react-native'; import Animated, { KeyboardState, SharedValue, runOnJS, useAnimatedKeyboard, useAnimatedProps, useAnimatedStyle, } from 'react-native-reanimated'; import { SafeAreaProvider, SafeAreaView, initialWindowMetrics, useSafeAreaInsets, } from 'react-native-safe-area-context'; import {Colors} from 'react-native/Libraries/NewAppScreen'; function App(): React.JSX.Element { const isDarkMode = useColorScheme() === 'dark'; const backgroundColor = isDarkMode ? Colors.darker : Colors.lighter; const styles = StyleSheet.create({ root: {flex: 1, backgroundColor}, container: {flex: 1}, }); return ( <SafeAreaProvider style={styles.root} initialMetrics={initialWindowMetrics}> <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={backgroundColor} translucent /> <SafeAreaView style={styles.container}> <AppContent isDarkMode={isDarkMode} /> </SafeAreaView> </SafeAreaProvider> ); } interface AppContentProps { isDarkMode: boolean; } const AppContent: React.FC<AppContentProps> = ({isDarkMode}) => { const styles = StyleSheet.create({ container: { flex: 1, }, content: { flex: 1, justifyContent: 'flex-end', backgroundColor: isDarkMode ? Colors.black : Colors.white, padding: 16, }, input: { height: 40, paddingHorizontal: 8, borderRadius: 4, borderColor: 'gray', borderWidth: 1, }, }); const topInset = useSafeAreaInsets().top; const keyboardVerticalOffset = topInset + 8; return ( <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.container}> <View style={styles.content}> <KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={keyboardVerticalOffset}> <Toolbar /> <TextInput placeholder="Type here..." style={styles.input} /> </KeyboardAvoidingView> </View> </ScrollView> ); }; const Toolbar: React.FC = () => { const styles = StyleSheet.create({ toolbar: { position: 'absolute', top: 0, left: 0, right: 0, height: 100, backgroundColor: 'red', transform: [{translateY: -120}], justifyContent: 'center', alignItems: 'center', }, }); const keyboard = useAnimatedKeyboard({isStatusBarTranslucentAndroid: true}); const animatedProps = useAnimatedProps(() => ({ pointerEvents: isKeyboardOpen(keyboard.state, keyboard.height) ? ('box-none' as const) : ('none' as const), })); const animatedStyle = useAnimatedStyle(() => ({ opacity: isKeyboardOpen(keyboard.state, keyboard.height) ? 1 : 0, })); return ( <Animated.View style={[styles.toolbar, animatedStyle]} animatedProps={animatedProps}> <Text>Toolbar visible when keyboard is open</Text> </Animated.View> ); }; /* export */ export default App; /* utils */ function isKeyboardOpen( state: SharedValue<KeyboardState>, height: SharedValue<number>, ): boolean { 'worklet'; runOnJS(debug)('Keyboard State', state.value); runOnJS(debug)('Keyboard Height', height.value); return ( state.value === KeyboardState.OPEN || state.value === KeyboardState.OPENING ); } /* debug */ function debug(label: string, value: string | number): void { console.log('🐞', label, ':', value); } ```
- Loading branch information