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
相关产品推荐
相关产品推荐

