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

[Android][Keyboard] More consistent inequality check to compute keyboard state #5874

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

antFrancon
Copy link

@antFrancon antFrancon commented Apr 8, 2024

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, 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:

// 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 android-keyboard

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.

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.

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);
}

@piaskowyk piaskowyk self-requested a review April 8, 2024 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant