From 1e48274223ee647ac4fc2c21822b5240f3c47e4c Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 11 Aug 2022 02:22:20 -0700 Subject: [PATCH] Use WindowInsetsCompat for Keyboard Events Summary: RN for Android fires `keyboardDidShow` and `keyboardDidHide` by observing changes to viewable window size. This isn't always reliable, such as when an activity has `awindowSoftInputMode` set to not have the system adjust the viewport when a keyboard is opened. Android 11 added the direct ability to measure and check visibility of the soft keyboard via `WindowInsets`, which fixes these issues. This is exposed downlevel to API 23 via WindowInsetsComapt` with the same limitations as previously, but using it simplifies our calculations a lot. Changelog: [Android][Fixed] - Use WindowInsetsCompat for Keyboard Events Reviewed By: javache Differential Revision: D38500859 fbshipit-source-id: d4ad41d7e75e4b9c14a485539a5f9de19de74362 --- Libraries/Components/Keyboard/Keyboard.js | 9 +-- .../com/facebook/react/ReactRootView.java | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index e7a7881742a..d80f9356779 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -126,10 +126,11 @@ class Keyboard { * - `keyboardWillChangeFrame` * - `keyboardDidChangeFrame` * - * Note that if you set `android:windowSoftInputMode` to `adjustResize` or `adjustNothing`, - * only `keyboardDidShow` and `keyboardDidHide` events will be available on Android. - * `keyboardWillShow` as well as `keyboardWillHide` are generally not available on Android - * since there is no native corresponding event. + * Android versions prior to API 30 rely on observing layout changes when + * `android:windowSoftInputMode` is set to `adjustResize` or `adjustPan`. + * + * `keyboardWillShow` as well as `keyboardWillHide` are not available on Android since there is + * no native corresponding event. * * @param {function} callback function to be called when the event fires. */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index f746f398f7c..1114d8e1532 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -12,6 +12,7 @@ import static com.facebook.react.uimanager.common.UIManagerType.DEFAULT; import static com.facebook.react.uimanager.common.UIManagerType.FABRIC; import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; +import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; @@ -31,6 +32,9 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.graphics.Insets; +import androidx.core.view.WindowInsetsCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.ThreadConfined; @@ -770,7 +774,11 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { @VisibleForTesting /* package */ void simulateCheckForKeyboardForTesting() { - getCustomGlobalLayoutListener().checkForKeyboardEvents(); + if (Build.VERSION.SDK_INT >= 23) { + getCustomGlobalLayoutListener().checkForKeyboardEvents(); + } else { + getCustomGlobalLayoutListener().checkForKeyboardEventsLegacy(); + } } private CustomGlobalLayoutListener getCustomGlobalLayoutListener() { @@ -879,7 +887,8 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { private final Rect mVisibleViewArea; private final int mMinKeyboardHeightDetected; - private int mKeyboardHeight = 0; + private boolean mKeyboardIsVisible = false; + private int mKeyboardHeight = 0; // Only used in checkForKeyboardEventsLegacy path private int mDeviceRotation = 0; /* package */ CustomGlobalLayoutListener() { @@ -895,13 +904,62 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { || mReactInstanceManager.getCurrentReactContext() == null) { return; } - checkForKeyboardEvents(); + + // WindowInsetsCompat IME measurement is reliable for API level 23+. + // https://developer.android.com/jetpack/androidx/releases/core#1.5.0-alpha02 + if (Build.VERSION.SDK_INT >= 23) { + checkForKeyboardEvents(); + } else { + checkForKeyboardEventsLegacy(); + } + checkForDeviceOrientationChanges(); checkForDeviceDimensionsChanges(); } + @RequiresApi(api = Build.VERSION_CODES.M) private void checkForKeyboardEvents() { getRootView().getWindowVisibleDisplayFrame(mVisibleViewArea); + WindowInsets rootInsets = getRootView().getRootWindowInsets(); + WindowInsetsCompat compatRootInsets = WindowInsetsCompat.toWindowInsetsCompat(rootInsets); + + boolean keyboardIsVisible = compatRootInsets.isVisible(WindowInsetsCompat.Type.ime()); + if (keyboardIsVisible != mKeyboardIsVisible) { + mKeyboardIsVisible = keyboardIsVisible; + + if (keyboardIsVisible) { + Insets imeInsets = compatRootInsets.getInsets(WindowInsetsCompat.Type.ime()); + Insets barInsets = compatRootInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + int height = imeInsets.bottom - barInsets.bottom; + + int softInputMode = ((Activity) getContext()).getWindow().getAttributes().softInputMode; + int screenY = + softInputMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING + ? mVisibleViewArea.bottom - height + : mVisibleViewArea.bottom; + + sendEvent( + "keyboardDidShow", + createKeyboardEventPayload( + PixelUtil.toDIPFromPixel(screenY), + PixelUtil.toDIPFromPixel(mVisibleViewArea.left), + PixelUtil.toDIPFromPixel(mVisibleViewArea.width()), + PixelUtil.toDIPFromPixel(height))); + } else { + sendEvent( + "keyboardDidHide", + createKeyboardEventPayload( + PixelUtil.toDIPFromPixel(mLastHeight), + 0, + PixelUtil.toDIPFromPixel(mVisibleViewArea.width()), + 0)); + } + } + } + + private void checkForKeyboardEventsLegacy() { + getRootView().getWindowVisibleDisplayFrame(mVisibleViewArea); + int notchHeight = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowInsets insets = getRootView().getRootWindowInsets(); @@ -919,8 +977,10 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { boolean isKeyboardShowingOrKeyboardHeightChanged = mKeyboardHeight != heightDiff && heightDiff > mMinKeyboardHeightDetected; + if (isKeyboardShowingOrKeyboardHeightChanged) { mKeyboardHeight = heightDiff; + mKeyboardIsVisible = true; sendEvent( "keyboardDidShow", createKeyboardEventPayload( @@ -934,6 +994,7 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { boolean isKeyboardHidden = mKeyboardHeight != 0 && heightDiff <= mMinKeyboardHeightDetected; if (isKeyboardHidden) { mKeyboardHeight = 0; + mKeyboardIsVisible = false; sendEvent( "keyboardDidHide", createKeyboardEventPayload(