From 21662917aaac685ef23c87f134ad66344915173b Mon Sep 17 00:00:00 2001 From: Brian Yeh Date: Mon, 27 Apr 2026 21:30:22 +0800 Subject: [PATCH 1/2] fix: use WindowInsetsCompat to exclude keyboard insets on Android API 23-29 --- android/build.gradle | 1 + .../safeareacontext/SafeAreaUtils.kt | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index f324b9a9..bad2cab9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -121,4 +121,5 @@ def kotlin_version = getExtOrDefault('kotlinVersion', project.properties['RNSAC_ dependencies { implementation 'com.facebook.react:react-native:+' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.9.0' } diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt index 1fef8dcd..e85db954 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt @@ -5,6 +5,8 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import androidx.annotation.RequiresApi +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import java.lang.IllegalArgumentException import kotlin.math.max import kotlin.math.min @@ -28,15 +30,31 @@ private fun getRootWindowInsetsCompatR(rootView: View): EdgeInsets? { @RequiresApi(Build.VERSION_CODES.M) @Suppress("DEPRECATION") private fun getRootWindowInsetsCompatM(rootView: View): EdgeInsets? { + // Use WindowInsetsCompat to reliably exclude IME (keyboard) insets on API 23-29. + // The deprecated min(systemWindowInsetBottom, stableInsetBottom) approach can + // incorrectly report keyboard height as the bottom inset on some Android 10 + // devices (e.g. Samsung One UI, Nokia with stock Android), causing SafeAreaView + // to add paddingBottom equal to the keyboard height and push screen content up. + // WindowInsetsCompat.Type.navigationBars() explicitly excludes IME insets, + // matching the behaviour of getRootWindowInsetsCompatR on API 30+. + val windowInsetsCompat = ViewCompat.getRootWindowInsets(rootView) + if (windowInsetsCompat != null) { + val insets = + windowInsetsCompat.getInsets( + WindowInsetsCompat.Type.statusBars() or + WindowInsetsCompat.Type.displayCutout() or + WindowInsetsCompat.Type.navigationBars()) + return EdgeInsets( + top = insets.top.toFloat(), + right = insets.right.toFloat(), + bottom = insets.bottom.toFloat(), + left = insets.left.toFloat()) + } + // Fallback for cases where ViewCompat.getRootWindowInsets() is unavailable. val insets = rootView.rootWindowInsets ?: return null return EdgeInsets( top = insets.systemWindowInsetTop.toFloat(), right = insets.systemWindowInsetRight.toFloat(), - // System insets are more reliable to account for notches but the - // system inset for bottom includes the soft keyboard which we don't - // want to be consistent with iOS. Using the min value makes sure we - // never get the keyboard offset while still working with devices that - // hide the navigation bar. bottom = min(insets.systemWindowInsetBottom, insets.stableInsetBottom).toFloat(), left = insets.systemWindowInsetLeft.toFloat()) } From eba9d5801623e3550f0d36fa8173694a4318906f Mon Sep 17 00:00:00 2001 From: Brian Yeh Date: Mon, 27 Apr 2026 21:41:43 +0800 Subject: [PATCH 2/2] refactor: narrow fix to only correct bottom inset, keep systemWindowInset* for top/right/left --- .../safeareacontext/SafeAreaUtils.kt | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt index e85db954..dfe6eb9b 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt @@ -30,32 +30,24 @@ private fun getRootWindowInsetsCompatR(rootView: View): EdgeInsets? { @RequiresApi(Build.VERSION_CODES.M) @Suppress("DEPRECATION") private fun getRootWindowInsetsCompatM(rootView: View): EdgeInsets? { - // Use WindowInsetsCompat to reliably exclude IME (keyboard) insets on API 23-29. + val insets = rootView.rootWindowInsets ?: return null + // Use WindowInsetsCompat to calculate the bottom inset without keyboard height. // The deprecated min(systemWindowInsetBottom, stableInsetBottom) approach can // incorrectly report keyboard height as the bottom inset on some Android 10 // devices (e.g. Samsung One UI, Nokia with stock Android), causing SafeAreaView // to add paddingBottom equal to the keyboard height and push screen content up. - // WindowInsetsCompat.Type.navigationBars() explicitly excludes IME insets, - // matching the behaviour of getRootWindowInsetsCompatR on API 30+. - val windowInsetsCompat = ViewCompat.getRootWindowInsets(rootView) - if (windowInsetsCompat != null) { - val insets = - windowInsetsCompat.getInsets( - WindowInsetsCompat.Type.statusBars() or - WindowInsetsCompat.Type.displayCutout() or - WindowInsetsCompat.Type.navigationBars()) - return EdgeInsets( - top = insets.top.toFloat(), - right = insets.right.toFloat(), - bottom = insets.bottom.toFloat(), - left = insets.left.toFloat()) - } - // Fallback for cases where ViewCompat.getRootWindowInsets() is unavailable. - val insets = rootView.rootWindowInsets ?: return null + // WindowInsetsCompat.Type.navigationBars() explicitly excludes IME insets. + val bottomInset = + ViewCompat.getRootWindowInsets(rootView) + ?.getInsets( + WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.displayCutout()) + ?.bottom + ?.toFloat() + ?: min(insets.systemWindowInsetBottom, insets.stableInsetBottom).toFloat() return EdgeInsets( top = insets.systemWindowInsetTop.toFloat(), right = insets.systemWindowInsetRight.toFloat(), - bottom = min(insets.systemWindowInsetBottom, insets.stableInsetBottom).toFloat(), + bottom = bottomInset, left = insets.systemWindowInsetLeft.toFloat()) }