Android横屏模式键盘可见性检测适配异常问题排查
Hey there, let's work through this tricky keyboard visibility detection issue you're hitting for your SDK. I've dealt with similar edge cases in the past, so here's a robust solution that works across all API levels without forcing your users to mess with manifest flags or layout attributes:
First, let's understand why this happens:
- On API 21-29 mobile devices in landscape mode, when the keyboard fills the screen alongside an EditText, the system doesn't trigger
OnGlobalLayoutListenerorOnApplyWindowInsetsListener(withoutadjustResize). This is because the window's visible area doesn't change enough to trigger the listeners—unlike API 30+, where Google improved window insets handling specifically for these edge cases. - The official WindowInsetsAnimation samples don't help here because they rely on the same system behavior that's broken for older API levels in this scenario.
We'll use a hybrid approach that combines reliable checks for each API tier, abstracted into a reusable utility your SDK can expose.
1. API 30+: Use WindowInsetsCompat (No Hoop-Jumping Needed)
For Android 11 and above, WindowInsetsCompat works consistently without any manifest tweaks. Set up a listener directly on the activity's root view:
val rootView = activity.window.decorView.rootView ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) // Trigger your SDK's visibility callback here insets // Always return the insets to allow proper handling by the system }
2. API 21-29: Enhanced Global Layout Listener
For older versions, we need to beef up the OnGlobalLayoutListener with a threshold check to detect keyboard presence even when the window size doesn't change drastically:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { val rootView = activity.window.decorView.rootView val viewTreeObserver = rootView.viewTreeObserver var previousKeyboardState = false viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { val displayFrame = Rect() rootView.getWindowVisibleDisplayFrame(displayFrame) val screenHeight = rootView.rootView.height val keyboardHeight = screenHeight - displayFrame.bottom // Use a 15% screen height threshold to filter out small layout shifts (e.g., status bar changes) val currentKeyboardState = keyboardHeight > screenHeight * 0.15 // Only trigger a change if the state actually flipped if (currentKeyboardState != previousKeyboardState) { previousKeyboardState = currentKeyboardState // Trigger your SDK's visibility callback here } } }) }
3. Fallback Check for Stubborn Landscape Mobile Cases
On some API 21-29 mobile devices in landscape, even the enhanced listener might miss events. Add a safety check using the Input Method Manager:
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager // Check current keyboard state val isKeyboardOpen = imm.isAcceptingText // Optional: Listen for system dialog close events (covers cases where keyboard is dismissed via home/recents) val systemDialogFilter = IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) context.registerReceiver(object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val reason = intent?.getStringExtra("reason") if (reason in listOf("globalactions", "homekey")) { // Re-check keyboard state here to catch dismissals } } }, systemDialogFilter)
4. Wrap It All Into an SDK Utility
Abstract this logic into a clean class so your SDK users don't have to deal with API differences:
class KeyboardVisibilityDetector( private val context: Context, private val onVisibilityChanged: (isVisible: Boolean) -> Unit ) { private var currentKeyboardState = false fun startDetection() { when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> setupInsetsListener() else -> setupGlobalLayoutListener() } // Optional: Register the system dialog receiver here } private fun setupInsetsListener() { val rootView = (context as Activity).window.decorView.rootView ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> val newState = insets.isVisible(WindowInsetsCompat.Type.ime()) updateState(newState) insets } } private fun setupGlobalLayoutListener() { val rootView = (context as Activity).window.decorView.rootView rootView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { val displayFrame = Rect() rootView.getWindowVisibleDisplayFrame(displayFrame) val screenHeight = rootView.height val keyboardHeight = screenHeight - displayFrame.bottom val newState = keyboardHeight > screenHeight * 0.15 updateState(newState) } }) } private fun updateState(newState: Boolean) { if (newState != currentKeyboardState) { currentKeyboardState = newState onVisibilityChanged(newState) } } fun stopDetection() { // Clean up listeners and receivers here to prevent memory leaks val rootView = (context as Activity).window.decorView.rootView if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { rootView.viewTreeObserver.removeOnGlobalLayoutListener { /* reference your listener instance */ } } else { @Suppress("DEPRECATION") rootView.viewTreeObserver.removeGlobalOnLayoutListener { /* reference your listener instance */ } } context.unregisterReceiver(/* reference your broadcast receiver instance */) } }
- No Manifest Lock-In: This approach doesn't require users to set
adjustResize,fitsSystemWindows, or any other manifest attributes—perfect for SDKs that need to work across diverse app setups. - Threshold Tuning: The 15% screen height threshold can be adjusted based on your needs. Tweak it to filter out irrelevant layout shifts (like navigation bar hiding/showing) while still catching keyboard events.
- Memory Leak Prevention: Always make sure to clean up listeners and receivers when the detection is no longer needed (e.g., when the host app's activity is destroyed).
内容的提问来源于stack exchange,提问作者vallllll

