You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

Root Cause Breakdown

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 OnGlobalLayoutListener or OnApplyWindowInsetsListener (without adjustResize). 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.
Step-by-Step Solution

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 */)
    }
}
Critical Tips for SDK Development
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 16:07:54