如何在Jetpack Compose中检测键盘的打开和关闭?

33

在Jetpack Compose中,我发现唯一的方法是使用accompanist-insets来去除窗口插图。但这样会导致我应用程序布局出现其他问题。

安卓系统的做法似乎是这样,我可以将其传递到我的Compose应用程序中并根据情况采取行动。

在Jetpack Compose中还有其他方法吗?


2
WindowInsets.isImeVisible - k4dima
WindowInsets.isImeVisible 对我有效。 - bronze man
9个回答

91

更新

使用新的WindowInsets API,可以更加轻松地进行操作。

首先,要返回正确的值,需要设置:

WindowCompat.setDecorFitsSystemWindows(window, false)

然后将键盘用作状态:

@Composable
fun keyboardAsState(): State<Boolean> {
    val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
    return rememberUpdatedState(isImeVisible)
}

使用示例:

val isKeyboardOpen by keyboardAsState() // true or false

PS:我尝试过使用WindowInsets.isImeVisible,但第一次调用时它会返回true。


没有实验性API

如果你想使用语句,我找到了这个解决方案:

enum class Keyboard {
    Opened, Closed
}

@Composable
fun keyboardAsState(): State<Keyboard> {
    val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
    val view = LocalView.current
    DisposableEffect(view) {
        val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
            val rect = Rect()
            view.getWindowVisibleDisplayFrame(rect)
            val screenHeight = view.rootView.height
            val keypadHeight = screenHeight - rect.bottom
            keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
                Keyboard.Opened
            } else {
                Keyboard.Closed
            }
        }
        view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)

        onDispose {
            view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
        }
    }

    return keyboardState
}

而要检测/检查值,您只需要这样做:

val isKeyboardOpen by keyboardAsState() // Keyboard.Opened or Keyboard.Closed 

5
非常好的解决方案。谢谢。 - AbdulMomen عبدالمؤمن
5
做得非常好! - Mohammad Sianaki
1
记录 isKeyboardOpen 对我来说显示了同一个值的快速变化。就像每秒钟有10个日志。 - Tran Hoai Nam
哦,不用在意上面的内容,那是我的可组合实体正在快速重新组合。 - Tran Hoai Nam
1
@ujizin 感谢您的更新。我最终使用了 imePadding 来解决我的问题。我只需要找到一种根据键盘显示/隐藏来调整 UI 的方法,然后就找到了这个。 - Arst
显示剩余11条评论

24

这里有一个解决方案,它使用OnGlobalLayoutListener监听布局变化,并使用新的窗口插入API进行计算,正如文档所推荐的。您可以将此代码放置在 @Composable 函数的任何位置,并根据需要处理isKeyboardOpen。我测试过,在API 21及以上版本上可以正常工作。

val view = LocalView.current
val viewTreeObserver = view.viewTreeObserver
DisposableEffect(viewTreeObserver) {
    val listener = ViewTreeObserver.OnGlobalLayoutListener {
        val isKeyboardOpen = ViewCompat.getRootWindowInsets(view)
            ?.isVisible(WindowInsetsCompat.Type.ime()) ?: true
        // ... do anything you want here with `isKeyboardOpen`
    }

    viewTreeObserver.addOnGlobalLayoutListener(listener)
    onDispose {
        viewTreeObserver.removeOnGlobalLayoutListener(listener)
    }
}

对我来说,其他解决方案都不太好用:键盘总是关闭的。

  • 在基于 OnGlobalLayoutListener 的答案中,使用的公式似乎表现不如预期,并且使用了旧的 API
  • 在基于 WindowInsetListener 的答案中,由于 view 不是根视图,因此不会应用任何窗口插入。我尝试将 view 替换为 view.rootView,虽然键盘检测代码可以正常工作,但将根视图传递给 setOnApplyWindowInsetsListener 会替换组件设置的任何侦听器,这显然是不希望发生的。

1
这是纯粹的天才。 - ino
1
我无法感谢你提供的这个解决方案。 - ino
1
工作正常,代码编写得非常好。谢谢! - Lampione
1
注意!此示例存在内存泄漏问题:当视图/视图树观察器发生更改时,removeOnGlobalLayoutListener从错误的对象调用并未被删除的侦听器将导致视图泄漏。请使用已修复的版本:gist.github.com/MaxMyalkin/b63f4d4050e4ff882cf3a30abcab448f - m.myalkin
@m.myalkin 不同之处在于你只存储 view.viewTreeObserver 而不是整个 view,是吗?如果确认无误,我会更新我的答案。 - Stypox
1
@Stypox 还可以使用 DisposableEffect(viewTreeObserver)。从文档中得知:返回的 ViewTreeObserver 观察器不能保证在此 View 的生命周期内始终有效。也就是说,观察器可能会发生变化。 - m.myalkin

3
检测键盘是否打开或关闭可以使用WindowInsest.ime进行检查。
设置WindowCompat.setDecorFitsSystemWindows(window, false)
要检查它是打开还是关闭,请使用:
WindowInsets.isImeVisible

检查使用底部偏移量是否正在上升或打开,但它并不总是可靠的,您需要采取额外步骤来检查它是在打开还是关闭。
val offsetY = WindowInsets.ime.getBottom(density)

您可以比较以前的值,并检测它是开启还是关闭,打开或关闭。

https://dev59.com/b8Tsa4cB1Zd3GeqPNv3P#73358604

当它打开时,它会返回诸如以下的值。
17:40:21.429  I  OffsetY: 1017
17:40:21.446  I  OffsetY: 38
17:40:21.463  I  OffsetY: 222
17:40:21.479  I  OffsetY: 438
17:40:21.496  I  OffsetY: 586
17:40:21.513  I  OffsetY: 685
17:40:21.530  I  OffsetY: 764
17:40:21.546  I  OffsetY: 825
17:40:21.562  I  OffsetY: 869
17:40:21.579  I  OffsetY: 907
17:40:21.596  I  OffsetY: 937
17:40:21.613  I  OffsetY: 960
17:40:21.631  I  OffsetY: 979
17:40:21.646  I  OffsetY: 994
17:40:21.663  I  OffsetY: 1004
17:40:21.679  I  OffsetY: 1010
17:40:21.696  I  OffsetY: 1014
17:40:21.713  I  OffsetY: 1016
17:40:21.730  I  OffsetY: 1017
17:40:21.746  I  OffsetY: 1017

"关闭中"
17:40:54.276  I  OffsetY: 0
17:40:54.288  I  OffsetY: 972
17:40:54.303  I  OffsetY: 794
17:40:54.320  I  OffsetY: 578
17:40:54.337  I  OffsetY: 430
17:40:54.354  I  OffsetY: 331
17:40:54.371  I  OffsetY: 252
17:40:54.387  I  OffsetY: 191
17:40:54.404  I  OffsetY: 144
17:40:54.421  I  OffsetY: 109
17:40:54.437  I  OffsetY: 79
17:40:54.454  I  OffsetY: 55
17:40:54.471  I  OffsetY: 37
17:40:54.487  I  OffsetY: 22
17:40:54.504  I  OffsetY: 12
17:40:54.521  I  OffsetY: 6
17:40:54.538  I  OffsetY: 2
17:40:54.555  I  OffsetY: 0
17:40:54.571  I  OffsetY: 0

3
我用Android的viewTreeObserver找到了一种方法。 它本质上是Android版本,但它调用一个回调函数,在Compose中可以使用。
class MainActivity : ComponentActivity() {

  var kbGone = false
  var kbOpened: () -> Unit = {}
  var kbClosed: () -> Unit = {}

  override fun onCreate(state: Bundle?) {
    super.onCreate(state)
    setContent {
      kbClosed = {
        // dismiss the keyboard with LocalFocusManager for example
      }
      kbOpened = {
        // something
      }
      MyComponent()
    }
    setupKeyboardDetection(findViewById<View>(android.R.id.content))
  }

  fun setupKeyboardDetection(contentView: View) {
    contentView.viewTreeObserver.addOnGlobalLayoutListener {
      val r = Rect()
      contentView.getWindowVisibleDisplayFrame(r)
      val screenHeight = contentView.rootView.height
      val keypadHeight = screenHeight - r.bottom
      if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is perhaps enough to determine keypad height.
        kbGone = false
        kbOpened()
      } else(!kbGone) {
        kbGone = true
        kbClosed()
      }
    }
  }
}

2
为了处理Jetpack Compose中键盘的打开和关闭行为,而又不改变Activity中的`WindowCompat.setDecorFitsSystemWindows(window, false)`设置,特别是在包含Fragment和Compose的单一活动架构中工作时,您可以使用以下方法:
@Composable
fun keyboardAsState(): State<Boolean> {
    val view = LocalView.current
    var isImeVisible by remember { mutableStateOf(false) }

    DisposableEffect(LocalWindowInfo.current) {
        val listener = ViewTreeObserver.OnPreDrawListener {
            isImeVisible = ViewCompat.getRootWindowInsets(view)
                ?.isVisible(WindowInsetsCompat.Type.ime()) == true
            true
        }
        view.viewTreeObserver.addOnPreDrawListener(listener)
        onDispose {
            view.viewTreeObserver.removeOnPreDrawListener(listener)
        }
    }
    return rememberUpdatedState(isImeVisible)
}

这段代码提供了一个可组合的函数keyboardAsState,允许您观察和响应键盘可见性状态的变化。它通过利用DisposableEffectViewTreeObserver.OnPreDrawListener来检测键盘打开或关闭的时机。
要使用此函数,只需在需要跟踪键盘状态的可组合项中将其值作为val isKeyboardOpen by keyboardAsState()访问即可。它将返回一个反映键盘当前是否可见的State<Boolean>

1
现在,有了新的 WindowInsets API,可以使用 WindowInsets.isImeVisible。参考:此链接

0
在Jetpack Compose中:
@Composable
fun isKeyboardVisible(): Boolean = WindowInsets.ime.getBottom(LocalDensity.current) > 0

它会返回true或false,

True -> 键盘可见

False -> 键盘不可见


如果需要监控呢? - gaohomway
你能详细解释一下吗? - Tippu Fisal Sheriff

0

我们还可以使用WindowInsetListener,就像这样

@Composable
fun keyboardAsState(): State<Boolean> {
    val keyboardState = remember { mutableStateOf(false) }
    val view = LocalView.current
    LaunchedEffect(view) {
        ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
            keyboardState.value = insets.isVisible(WindowInsetsCompat.Type.ime())
            insets
        }
    }
    return keyboardState
}

1
不起作用。无论键盘是否可见,状态都保持不变。 - ino
因为你必须在MainActivity中设置WindowCompat.setDecorFitsSystemWindows(window, false) - mama

0
给你:
@Composable
fun OnKeyboardClosedEffect(block: () -> Unit) {
    val isKeyboardVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 130
    var keyboardListenerHaBeenSet by remember { mutableStateOf(false) }
    if (isKeyboardVisible || keyboardListenerHaBeenSet) {
        if (!isKeyboardVisible) {
            block()
            keyboardListenerHaBeenSet = false // clear
        }
        keyboardListenerHaBeenSet = true
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接