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

Jetpack Compose测试:验证软键盘的显示与隐藏状态

解决软键盘状态测试不稳定的方案

1. 自定义键盘状态IdleResource

系统没有预定义的键盘IdleResource,我们可以自己实现一个,让Espresso等待键盘状态稳定后再执行断言。核心是监听键盘可见性变化,直到状态脱离过渡态(opening/closing),或连续多次检测到相同的稳定状态(open/closed)。

示例代码:

class KeyboardIdleResource : IdlingResource {
    private var callback: IdlingResource.ResourceCallback? = null
    private var lastKeyboardState: KeyboardState? = null
    private var stableCount = 0

    enum class KeyboardState { OPEN, CLOSED, TRANSITION }

    override fun getName(): String = "KeyboardIdleResource"

    override fun isIdleNow(): Boolean {
        val currentState = getCurrentKeyboardState()
        return when {
            currentState == KeyboardState.TRANSITION -> {
                stableCount = 0
                false
            }
            currentState == lastKeyboardState -> {
                stableCount++
                stableCount >= 2 // 连续两次检测到相同稳定状态,判定为已稳定
            }
            else -> {
                lastKeyboardState = currentState
                stableCount = 1
                false
            }
        }.also { isIdle ->
            if (isIdle) callback?.onTransitionToIdle()
        }
    }

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
        this.callback = callback
    }

    private fun getCurrentKeyboardState(): KeyboardState {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val windowInsets = ViewCompat.getRootWindowInsets((context as Activity).window.decorView)
        return if (windowInsets == null) {
            KeyboardState.CLOSED
        } else {
            val isVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
            val isAnimating = windowInsets.isAnimating()
            when {
                isAnimating -> KeyboardState.TRANSITION
                isVisible -> KeyboardState.OPEN
                else -> KeyboardState.CLOSED
            }
        }
    }
}

测试中注册/注销:

@Before
fun setup() {
    val keyboardIdleResource = KeyboardIdleResource()
    IdlingRegistry.getInstance().register(keyboardIdleResource)
}

@After
fun teardown() {
    val keyboardIdleResource = IdlingRegistry.getInstance().getResources().find { it is KeyboardIdleResource }
    keyboardIdleResource?.let { IdlingRegistry.getInstance().unregister(it) }
}

2. 改进Shell命令检测方案

之前的isKeyboardOpenedShellCheck()出现Receiver未注册异常,是因为广播注册/注销时机不当。可以改用解析dumpsys input_method输出的方式,无需注册广播,同时增加等待逻辑直到状态稳定。

示例代码:

fun waitForKeyboardStable(targetState: Boolean, timeout: Long = 5000) {
    val endTime = System.currentTimeMillis() + timeout
    while (System.currentTimeMillis() < endTime) {
        val isKeyboardOpen = isKeyboardOpenViaShell()
        val isTransitioning = isKeyboardTransitioningViaShell()
        if (!isTransitioning && isKeyboardOpen == targetState) {
            return
        }
        Thread.sleep(100)
    }
    throw AssertionError("Keyboard did not reach target state $targetState within timeout")
}

private fun isKeyboardOpenViaShell(): Boolean {
    val output = InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
        "dumpsys input_method | grep mInputShown"
    ).bufferedReader().readText()
    return output.contains("mInputShown=true")
}

private fun isKeyboardTransitioningViaShell(): Boolean {
    val output = InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
        "dumpsys input_method | grep mIsAnimating"
    ).bufferedReader().readText()
    return output.contains("mIsAnimating=true")
}

测试调用:

// 点击隐藏键盘按钮后,等待状态稳定
onView(withId(R.id.hide_keyboard_btn)).perform(click())
waitForKeyboardStable(targetState = false)
// 执行后续断言逻辑

3. 利用WindowInsets监听等待状态变化

在测试中给Activity根View设置WindowInsets监听器,等待键盘进入目标稳定状态。

示例代码:

fun waitForKeyboardState(targetVisible: Boolean, activity: Activity) {
    val latch = CountDownLatch(1)
    val decorView = activity.window.decorView
    ViewCompat.setOnApplyWindowInsetsListener(decorView) { _, insets ->
        val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
        val isAnimating = insets.isAnimating()
        if (!isAnimating && isVisible == targetVisible) {
            latch.countDown()
        }
        insets
    }
    latch.await(5, TimeUnit.SECONDS)
    ViewCompat.setOnApplyWindowInsetsListener(decorView, null) // 移除监听
}

测试调用:

val activity = activityRule.activity
onView(withId(R.id.hide_keyboard_btn)).perform(click())
waitForKeyboardState(targetVisible = false, activity)
// 执行断言逻辑

注意事项

  • 所有方案都必须等待过渡态结束再执行断言,避免在opening/closing阶段检测导致的不稳定;
  • 不同设备的dumpsys input_method输出字段可能有差异,需根据实际设备调整;
  • 自定义IdleResource最贴合Espresso测试框架,推荐优先使用。

内容的提问来源于stack exchange,提问作者18446744073709551615

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 13:55:18