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

Fix calculating keyboard height #5851

Conversation

maciekstosio
Copy link
Contributor

@maciekstosio maciekstosio commented Mar 29, 2024

Summary

Should fix: #5811

Keyboard height seems to be improperly calculated. Based on my investigation it seems that WindowInsetsCompat.Type.ime() includes height of the keyboard + bottom navigation. What Reanimated does currently is checking if navigation bar is visible (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME);) and then subtracts the bottom inset int keyboardHeight = DiphasNavigationBar ? contentBottomInset - systemBarBottomInset : contentBottomInset;. But to my understanding hasNavigationBar is always true, thus when keyboard is hidden keyboard height becomes -contentBottomInset. So I changed the navigation height to be Math.max(0, keyboardHeightDip)

Additionally in WindowInsetsManager.java I applied default paddings and added padding bottom which should help correctly position element from the bottom.

Remarks

  • on older androids < 11, hidden status bar rolls back to adjustPan keyboard behavior
  • flag isStatusBarTranslucentAndroid doesn't seems to do anything when screen has native header (headerShown: true)

Examples before vs after

Adding useAnimatedKeyboard breaks the layout

Before After
adding.useAnimatedKeyboard.Before.mov
adding.useAnimatedKeyboard.After.mov

After closing the keyboard there is unexpected gap (negative height)

Before After
topSpace.Before.mov
topSpace.After.mov

When StatusBar is hidden keyboard nor working properly on Android 10 - not fixed but not a regression

Before After
statusBar.Before.mov
statusBar.after.mov

Test plan

Example showing the problem
import React, { useEffect, useState } from 'react';
import Animated, {
  useAnimatedKeyboard,
  useAnimatedReaction,
  useAnimatedStyle,
} from 'react-native-reanimated';
import {
  Platform,
  StatusBar,
  StyleSheet,
  TextInput,
  View,
  Text,
  Button,
} from 'react-native';

export default function EmptyExample() {
  const [statusBarHidden, setStatusBarHidden] = useState(false);
  const keyboard = useAnimatedKeyboard();

  const animatedStyles = useAnimatedStyle(() => ({
    transform: [{ translateY: -keyboard.height.value }],
  }));

  useAnimatedReaction(
    () => {
      return keyboard.height.value;
    },
    (currentValue) => console.log(currentValue)
  );

  useEffect(() => {
    StatusBar.setHidden(statusBarHidden);
  }, [statusBarHidden]);

  return (
    <Animated.View
      style={[
        styles.container,
        animatedStyles,
        { justifyContent: 'flex-end', borderWidth: 5, borderColor: '#ff0' },
      ]}>
      <Button
        title="Toggle StatusBar"
        onPress={() => setStatusBarHidden((hidden) => !hidden)}
      />
      <Button
        title="Show Warning"
        onPress={() => console.warn('WARNING!!!!')}
      />
      <View
        style={[
          styles.center,
          {
            height: 200,
            backgroundColor: '#f0f',
            borderWidth: 5,
            borderColor: '#0ff',
          },
        ]}>
        <Text>{`Android ${Platform.constants['Release']}`}</Text>
        <TextInput placeholder="Test" />
      </View>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  center: { justifyContent: 'center', alignItems: 'center' },
});

@piaskowyk piaskowyk self-requested a review March 29, 2024 11:07
@maciekstosio maciekstosio marked this pull request as ready for review April 3, 2024 16:21
@kesha-antonov
Copy link

kesha-antonov commented Apr 5, 2024

We need this fix 🙏

Temp fix:

res/values/styles.xml:

    <style name="AppFullScreenTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowActionBar">true</item>
        <item name="android:windowFullscreen">false</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowTranslucentNavigation">true</item>
    </style>

AndroidManifest.xml:

<application
    android:theme="@style/AppFullScreenTheme"
  1. Wrap app's content in SafeAreaView

  2. Height for chat's container:

const containerStyle = useAnimatedStyle(() => ({
    height: contentHeightAnim.value - keyboard.height.value + safeAreaInsets.bottom + (keyboard.height.value > 0 ? safeAreaInsets.bottom : 0),
  }))

@maciekstosio
Copy link
Contributor Author

@kesha-antonov you can also try to use patch from below. Please let me know if that helps with your issue.

Patch
diff --git a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java
index 0f8ae7807..d71bd5f07 100644
--- a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java
+++ b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java
@@ -1,7 +1,5 @@
 package com.swmansion.reanimated.keyboard;
 
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
 import androidx.core.view.WindowInsetsCompat;
 import com.facebook.react.uimanager.PixelUtil;
 
@@ -23,9 +21,7 @@ public class Keyboard {
   public void updateHeight(WindowInsetsCompat insets) {
     int contentBottomInset = insets.getInsets(CONTENT_TYPE_MASK).bottom;
     int systemBarBottomInset = insets.getInsets(SYSTEM_BAR_TYPE_MASK).bottom;
-    boolean hasNavigationBar = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME);
-    int keyboardHeightDip =
-        hasNavigationBar ? contentBottomInset - systemBarBottomInset : contentBottomInset;
+    int keyboardHeightDip = contentBottomInset - systemBarBottomInset;
     int keyboardHeight = (int) PixelUtil.toDIPFromPixel(Math.max(0, keyboardHeightDip));
     if (keyboardHeight == 0 && mState == KeyboardState.OPEN) {
       /*
@@ -35,7 +31,7 @@ public class Keyboard {
       */
       return;
     }
-    mHeight = (int) PixelUtil.toDIPFromPixel(keyboardHeightDip);
+    mHeight = keyboardHeight;
   }
 
   public void onAnimationStart() {
diff --git a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java
index b7ad125e9..353084cf0 100644
--- a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java
+++ b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java
@@ -9,7 +9,6 @@ import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowCompat;
 import androidx.core.view.WindowInsetsCompat;
 import com.facebook.react.bridge.ReactApplicationContext;
-import com.swmansion.reanimated.BuildConfig;
 import java.lang.ref.WeakReference;
 
 public class WindowsInsetsManager {
@@ -58,24 +57,19 @@ public class WindowsInsetsManager {
   }
 
   private WindowInsetsCompat onApplyWindowInsetsListener(View view, WindowInsetsCompat insets) {
+    WindowInsetsCompat defaultInsets = ViewCompat.onApplyWindowInsets(view, insets);
     if (mKeyboard.getState() == KeyboardState.OPEN) {
       mKeyboard.updateHeight(insets);
       mNotifyAboutKeyboardChange.call();
     }
-    setWindowInsets(insets);
-    return insets;
+    setWindowInsets(defaultInsets);
+    return defaultInsets;
   }
 
   private void setWindowInsets(WindowInsetsCompat insets) {
-    int paddingBottom = 0;
-    boolean isOldPaperImplementation =
-        !BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && BuildConfig.REACT_NATIVE_MINOR_VERSION < 70;
-    if (isOldPaperImplementation) {
-      int navigationBarTypeMask = WindowInsetsCompat.Type.navigationBars();
-      paddingBottom = insets.getInsets(navigationBarTypeMask).bottom;
-    }
     int systemBarsTypeMask = WindowInsetsCompat.Type.systemBars();
     int paddingTop = insets.getInsets(systemBarsTypeMask).top;
+    int paddingBottom = insets.getInsets(systemBarsTypeMask).bottom;
     updateInsets(paddingTop, paddingBottom);
   }
 
@@ -95,7 +89,7 @@ public class WindowsInsetsManager {
     FrameLayout.LayoutParams params =
         new FrameLayout.LayoutParams(matchParentFlag, matchParentFlag);
     if (mIsStatusBarTranslucent) {
-      params.setMargins(0, 0, 0, 0);
+      params.setMargins(0, 0, 0, paddingBottom);
     } else {
       params.setMargins(0, paddingTop, 0, paddingBottom);
     }

@kesha-antonov
Copy link

@kesha-antonov you can also try to use patch from below. Please let me know if that helps with your issue.

Patch

diff --git a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java
index 0f8ae7807..d71bd5f07 100644
--- a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java
+++ b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/Keyboard.java
@@ -1,7 +1,5 @@
 package com.swmansion.reanimated.keyboard;
 
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
 import androidx.core.view.WindowInsetsCompat;
 import com.facebook.react.uimanager.PixelUtil;
 
@@ -23,9 +21,7 @@ public class Keyboard {
   public void updateHeight(WindowInsetsCompat insets) {
     int contentBottomInset = insets.getInsets(CONTENT_TYPE_MASK).bottom;
     int systemBarBottomInset = insets.getInsets(SYSTEM_BAR_TYPE_MASK).bottom;
-    boolean hasNavigationBar = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME);
-    int keyboardHeightDip =
-        hasNavigationBar ? contentBottomInset - systemBarBottomInset : contentBottomInset;
+    int keyboardHeightDip = contentBottomInset - systemBarBottomInset;
     int keyboardHeight = (int) PixelUtil.toDIPFromPixel(Math.max(0, keyboardHeightDip));
     if (keyboardHeight == 0 && mState == KeyboardState.OPEN) {
       /*
@@ -35,7 +31,7 @@ public class Keyboard {
       */
       return;
     }
-    mHeight = (int) PixelUtil.toDIPFromPixel(keyboardHeightDip);
+    mHeight = keyboardHeight;
   }
 
   public void onAnimationStart() {
diff --git a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java
index b7ad125e9..353084cf0 100644
--- a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java
+++ b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/keyboard/WindowsInsetsManager.java
@@ -9,7 +9,6 @@ import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowCompat;
 import androidx.core.view.WindowInsetsCompat;
 import com.facebook.react.bridge.ReactApplicationContext;
-import com.swmansion.reanimated.BuildConfig;
 import java.lang.ref.WeakReference;
 
 public class WindowsInsetsManager {
@@ -58,24 +57,19 @@ public class WindowsInsetsManager {
   }
 
   private WindowInsetsCompat onApplyWindowInsetsListener(View view, WindowInsetsCompat insets) {
+    WindowInsetsCompat defaultInsets = ViewCompat.onApplyWindowInsets(view, insets);
     if (mKeyboard.getState() == KeyboardState.OPEN) {
       mKeyboard.updateHeight(insets);
       mNotifyAboutKeyboardChange.call();
     }
-    setWindowInsets(insets);
-    return insets;
+    setWindowInsets(defaultInsets);
+    return defaultInsets;
   }
 
   private void setWindowInsets(WindowInsetsCompat insets) {
-    int paddingBottom = 0;
-    boolean isOldPaperImplementation =
-        !BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && BuildConfig.REACT_NATIVE_MINOR_VERSION < 70;
-    if (isOldPaperImplementation) {
-      int navigationBarTypeMask = WindowInsetsCompat.Type.navigationBars();
-      paddingBottom = insets.getInsets(navigationBarTypeMask).bottom;
-    }
     int systemBarsTypeMask = WindowInsetsCompat.Type.systemBars();
     int paddingTop = insets.getInsets(systemBarsTypeMask).top;
+    int paddingBottom = insets.getInsets(systemBarsTypeMask).bottom;
     updateInsets(paddingTop, paddingBottom);
   }
 
@@ -95,7 +89,7 @@ public class WindowsInsetsManager {
     FrameLayout.LayoutParams params =
         new FrameLayout.LayoutParams(matchParentFlag, matchParentFlag);
     if (mIsStatusBarTranslucent) {
-      params.setMargins(0, 0, 0, 0);
+      params.setMargins(0, 0, 0, paddingBottom);
     } else {
       params.setMargins(0, paddingTop, 0, paddingBottom);
     }

Thanks!

@kesha-antonov
Copy link

kesha-antonov commented Apr 5, 2024

@maciekstosio

is your keyboard animations on ios looks smooth ? cause on ios they're verry choppy in our app with same code
(it's not related to your PR)

@TomCorvus
Copy link

It doesn't work on my repro.
In my case, the warning message shows now in safe area view but the status bar is not includes in the print zone.
https://github.com/TomCorvus/RNSAV
these captures are done from a Google Pixel 7 with Android 14.

Before patching After patching with hook After patching without hook
share_3734606324019947891 share_5824319798550667949 share_1107710790529520659

@maciekstosio
Copy link
Contributor Author

@TomCorvus Hi, have you tried using isStatusBarTranslucentAndroid option set to true? I tried to describe better what it does in new docs #5866.

@TomCorvus
Copy link

This option fix the shift. Thanks @maciekstosio
Why do we need to set this option? It doesn't take the item in styles.xml or backgroundColor="transparent" prop from StatusBar react-native component?

@maciekstosio
Copy link
Contributor Author

maciekstosio commented Apr 9, 2024

@maciekstosio

is your keyboard animations on ios looks smooth ? cause on ios they're verry choppy in our app with same code (it's not related to your PR)

Hi, I focused on android implementation. It would be great if you could create separate issue if you're having problems on iOS.

@maciekstosio
Copy link
Contributor Author

maciekstosio commented Apr 9, 2024

@TomCorvus On Android, to listen to keyboard changes we use immersive mode. It expands root view to full screen and gives away the control over insets management, so we need to apply them ourselves:

  • When isStatusBarTranslucentAndroid is false we apply the top and bottom margins according to the insets.
  • When isStatusBarTranslucentAndroid is true we apply bottom padding according to the navigation inset and sets top margin to 0.

@maciekstosio
Copy link
Contributor Author

@TomCorvus I don't think I fully answered your question, it looks that we can do it automatically, you can see it in #5889.

@kesha-antonov
Copy link

kesha-antonov commented Apr 11, 2024

@maciekstosio

is your keyboard animations on ios looks smooth ? cause on ios they're verry choppy in our app with same code (it's not related to your PR)

It was related to bad optimization of chat lib. Made PR there: FaridSafi/react-native-gifted-chat#2493

Sorry for spamming your thread )

Copy link
Member

@piaskowyk piaskowyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏🎉

@piaskowyk piaskowyk added this pull request to the merge queue May 1, 2024
Merged via the queue into main with commit 69a1ae1 May 1, 2024
9 checks passed
@piaskowyk piaskowyk deleted the @maciekstosio/Investigate-unexpected-space-when-using-useAnimatedKeyboard branch May 1, 2024 08:11
github-merge-queue bot pushed a commit that referenced this pull request May 9, 2024
<!-- 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

This PR updates the `useAnimatedKeyboard` docs to current doc pattern +
add more detailed remarks discovered while doing #5851. It's pointing to
#5851 and assumes changes from that PR, so **should be merged after**.

## Test plan

```
cd docs
yarn
yarn start
```

and go to useAnimatedKeyboard section

<!-- Provide a minimal but complete code snippet that can be used to
test out this change along with instructions how to run it and a
description of the expected behavior. -->

---------

Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com>
Co-authored-by: Kacper Kapuściak <39658211+kacperkapusciak@users.noreply.github.com>
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.

Call useAnimatedKeyboard hook breaks screen area
4 participants