28

The only way I've found in compose is to use accompanist-insets and that removes window insets. And such causes other problems with my app's layout.

The Android way seems to be this and I could pass that into my compose app and act accordingly.

Is there another way in jetpack compose?

mmm111mmm
  • 3,607
  • 4
  • 27
  • 44

9 Answers9

76

Update

With the new WindowInsets API, it gets easier

First, to return the correct values, you need to set:

WindowCompat.setDecorFitsSystemWindows(window, false)

Then to use Keyboard as a state:

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

use example:

val isKeyboardOpen by keyboardAsState() // true or false

ps: I've tried to use WindowInsets.isImeVisible, but it returns true in the first call.


Without an experimental API

if you want with the statement, I found this solution:

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
}

and to detect/check the value you'll only need this:

val isKeyboardOpen by keyboardAsState() // Keyboard.Opened or Keyboard.Closed 
ujizin
  • 762
  • 4
  • 8
  • 5
    very nice solution. thanks. – AbdulMomen عبدالمؤمن Nov 03 '21 at 15:22
  • 5
    very nicely done! – Mohammad Sianaki Jan 05 '22 at 09:23
  • Logging `isKeyboardOpen` shows rapid changes of the same value for me. Like 10 logs every second. – Tran Hoai Nam Feb 11 '22 at 04:02
  • Oh never mind above, it was my composable being recompose rapidly – Tran Hoai Nam Feb 11 '22 at 04:58
  • It stops working when I lock screen and unlock it again. Any idea why? – matip Aug 29 '22 at 13:54
  • The state starts as closed but remains opened no matter the keyboard is shown or hidden. – ino Sep 11 '22 at 17:05
  • Weird behavior oh my end. When first time whos the compose, WindowInsets.isImeVisible show true, but there is no keyboard shows, when tap on an input field, nothing is triggered. After I close the keyboard, WindowInsets.isImeVisible shows false. And I can get values change foward. – Arst Nov 28 '22 at 08:44
  • Hey @Arst, I just updated the answer, could you try again? Please make sure that you set `setDecorFitsSystemWindows` correctly too – ujizin Nov 30 '22 at 00:26
  • I also pushed a sample here: https://github.com/ujizin/Compose-stackoverflow/blob/main/app/src/main/java/com/ujizin/compose_stackoverflow/presentation/keyboard_state/KeyboardStateScreen.kt – ujizin Nov 30 '22 at 00:33
  • 1
    @ujizin Thanks for the update. I ended up using `imePadding` for my case. I just needed to find way to adjust UI based on keyboard show/hide and found this one. – Arst Nov 30 '22 at 04:35
  • I get `Unresolved reference: ime` error and even after importing the recommended, this doesn't work. – Vaz Jan 08 '23 at 18:05
  • I do keep getting wrong states when going back from the screen with open keyboard. Needs to be improved – Giedrius Šlikas Feb 14 '23 at 07:51
  • Personaly the solution "Without an experimental API" works perfectly then the other is not working on my 29 api devices. May is some configuration that i'm using. Ty ! – MakiX Feb 23 '23 at 14:37
  • Caution! Non experimental example has memory leak: when view/viewtreeobserver changes removeOnGlobalLayoutListener is called from incorrect object and listener isn't deleted, that causes view leak. Use fixed version: https://gist.github.com/MaxMyalkin/b63f4d4050e4ff882cf3a30abcab448f – m.myalkin May 09 '23 at 17:55
  • The first option did work but it changes the screen size (edge-to-edge) which is not intended here, but the second option works, even though it shows a bit of lagginess but not really harmful – Raed Ghazal Jun 27 '23 at 20:42
22

Here is a solution that uses OnGlobalLayoutListener to listen to changes to the layout and uses the new window insets APIs to perform calculations, as recommended by the documentation. You can place this code anywhere inside a @Composable function and handle the isKeyboardOpen as you wish. I tested and it works on API 21 and above.

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)
    }
}

For me the other solutions wouldn't work well: the keyboard would result as always closed.

  • In OnGlobalLayoutListener-based answers, the used formula does not seem to behave as it should, and old APIs are used.
  • In the WindowInsetListener-based answer, since view is not the root view, no window insets would be applied on it. I tried replacing view with view.rootView, and although the keyboard-detection code would then work, passing the root view to setOnApplyWindowInsetsListener replaces any listener set by components, which is obviously unwanted.
Stypox
  • 963
  • 11
  • 18
  • 1
    This is pure genius – ino Sep 11 '22 at 21:44
  • 1
    I can't thank you enough for this solution. – ino Sep 11 '22 at 22:02
  • 1
    Working as expected and very well writter. Thank you! – Lampione Oct 17 '22 at 16:44
  • 1
    Caution! This example has memory leak: when view/viewtreeobserver changes removeOnGlobalLayoutListener is called from incorrect object and listener isn't deleted, that causes view leak. Use fixed version: gist.github.com/MaxMyalkin/b63f4d4050e4ff882cf3a30abcab448f – m.myalkin May 09 '23 at 17:57
  • @m.myalkin the difference is that you only store `view.viewTreeObserver` instead of the whole `view`, isn't it? If you confirm I will update my answer – Stypox May 09 '23 at 20:35
  • 1
    @Stypox Also use DisposableEffect(viewTreeObserver). From docs: The returned ViewTreeObserver observer is not guaranteed to remain valid for the lifetime of this View. I.e. observer can change – m.myalkin May 11 '23 at 18:52
4

I found a way with Android's viewTreeObserver. It essentially is the Android version but it calls a callback that can be used in 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()
      }
    }
  }
}
mmm111mmm
  • 3,607
  • 4
  • 27
  • 44
3

Detecting whether keyboard is opening or closing can be inspected with WindowInsest.ime

Set WindowCompat.setDecorFitsSystemWindows(window, false)

To check whether it's open or close use

WindowInsets.isImeVisible

Check if it's going up or opening with using bottom offset however it's not always reliable you need to do extra steps to check if it's opening or closing

val offsetY = WindowInsets.ime.getBottom(density)

you can compare a previous value and detect if it's opening closing, open or close

https://stackoverflow.com/a/73358604/5457853

When it opens it returns values such as

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

While closing

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
Thracian
  • 43,021
  • 16
  • 133
  • 222
1

Now, with the new WindowInsets api, WindowInsets.isImeVisible can be used. For reference, see this.

0

Also we can use WindowInsetListener, something like this

@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
}
Vadym
  • 543
  • 5
  • 16
  • 1
    Doesn't work. The state remains same whether or not keyboard is visible. – ino Sep 11 '22 at 21:47
  • Because you have to Set `WindowCompat.setDecorFitsSystemWindows(window, false)` in mainActivity – mama Nov 22 '22 at 17:00
0

In Jetpack compose:

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

It will return true or false,

True -> Keyboard Visible

False -> Keyboard Not Visible

Tippu Fisal Sheriff
  • 2,177
  • 11
  • 19
0

There you go:

@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
    }
}
Itay Cohen
  • 251
  • 1
  • 3
  • 13
0

To handle the keyboard opening and closing behavior in Jetpack Compose without altering the WindowCompat.setDecorFitsSystemWindows(window, false) setting in your Activity, especially when working with a single activity architecture containing Fragments and Composables, you can use the following approach:

@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)
}

This code snippet provides a Composable function keyboardAsState that allows you to observe and respond to changes in the keyboard's visibility status. It does so by leveraging the DisposableEffect and ViewTreeObserver.OnPreDrawListener to detect when the keyboard opens or closes.

To use this function, simply access its value as val isKeyboardOpen by keyboardAsState() within your Composable where you need to track the keyboard state. It will return a State<Boolean> that reflects whether the keyboard is currently visible or not.

Atri Tripathi
  • 13
  • 1
  • 5