Search code examples
androidandroid-jetpack-compose

Force soft keyboard presense in Compose


I am making search screen in POS app and want software keyboard to be always shown while TextField is present. Ideally I want to intercept event of user hiding keyboard to hide my search widget together with keyboard.

Currently I have

@Composable
fun Search(onHide: () -> Unit) {
    var query by rememberSaveable { mutableStateOf("") }
    BackHandler {
        onHide() //works only when keyboard is already hidden
    }
    Column(modifier = Modifier.fillMaxHeight()) {
        TextField(value = query, onValueChange = { query = it },
            modifier = Modifier.focusRequester(focusRequester))
        myItemsList.filter { it.title.startsWith(query) }.take(7).forEach {
            SearchLine(it)
        }
    }
    LaunchedEffect(key1 = focusRequester) {
        focusRequester.requestFocus()
        focusRequester.captureFocus()
//        keyboardController.show() //it is shown due to focus anyway
    }
}

For context, I use it like this:

fun Content() {
    var searchMode by rememberSaveable { mutableStateOf(true) } //FIXME false
    AppTheme {
        Surface {
            if (searchMode) {
                Search(onHide = { searchMode = false })
            } else {
                /* normally available activtiy contents */
            }
        }
    }
}

Bonus questions:

  • show/hide keyboard without animation
  • remove top bar of GBoard (actions, stickers, clipboard, voice input). KeyboardType.Password does the job for GBoard on my device, but it does not feel right in general.

These are probably relevant, because if it is also not possible, is my best option using custom in-app keyboard? I'd rather not, because of some tricky locales like Japanese.


I tried:

  1. BackHandler { <breakpoint> }. It is simply not triggered when user tries to hide keyboard.
  2. Modifier for TextField with focusRequester.captureFocus(). Idk if it helps to capture focus, my TextField is only focusable thing present and always focused anyway, but it does not prevent user from hiding keyboard.
  3. Fiddled with TextInputService.startInput(), there are fields onEditCommand and onImeActionPerformed, they are also not triggered when user hides keyboard
  4. Same goes for KeyboardActions(onAny = { <breakpoint> }), hiding does not trigger it.
  5. KeyboardOptions supplied to TextField() does not have relevant options
  6. I don't want separate activity for search, but tried android:windowSoftInputMode="stateAlwaysVisible" in Manifest anyway. It did nothing.
  7. LaunchedEffect(key1 = focusRequester) { keyboardController.show() } shows keyboard, but still hideable.
  8. imm!!.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) with window token of activtiy, did nothing, from my understending it is irrelevant for Compose.

And probably more, I am dog tired already:(


Solution

  • "Hide Keyboard" keycode is 17179869184 (Key.Back), so you can prevent hide keyboard action by intercepting the key. Image

    var value by remember { mutableStateOf("") }
    var isFocused by remember { mutableStateOf(false) }
    
    TextField(
        value = value,
        onValueChange = { value = it },
        modifier = Modifier
            .onPreInterceptKeyBeforeSoftKeyboard {
                if (it.key == Key.Back && isFocused) {
                    true
                } else {
                    false
                }
            }
            .onFocusChanged {
                isFocused = it.isFocused
                setSoftKeyboardVisibility(isFocused)
            }
    )
    
    private fun ComponentActivity.setSoftKeyboardVisibility(isVisible: Boolean) {
        currentFocus?.let { currentFocus ->
            with(WindowInsetsControllerCompat(window, currentFocus)) {
                if (isVisible) {
                    show(WindowInsetsCompat.Type.ime())
                } else {
                    hide(WindowInsetsCompat.Type.ime())
                }
            }
        }
    }
    

    Unfortunately, there are some cases that this solution does not cover. When gesture navigation bar feature is on, the keyboard is hidden by back swipe gesture. Image(Swipe Gesture)

    There might also be other way to hide the keyboard, not only per device but software keyboards. So, I think implementing custom in-app keyboard is the best way to ensure keyboard visibility.

    Edit:

    I found that onPreInterceptKeyBeforeSoftKeyboard is not properly triggered on some devices like Pixel 6 or Emulators. So I had to find an additional means to ensure keyboard visibility.

    Thanks to @trinadh thatakula, I came up with an idea to hide or show soft keyboard when WindowInsets of ime changes.

    val isImeVisible = WindowInsets.isImeVisible
    LaunchedEffect(isImeVisible) {
        setSoftKeyboardVisibility(isFocused)
    }
    

    WindowInsets.isImeVisible is marked as ExperimentalLayoutApi. so if you don't like it, you may use the following code.

    val imeInsets = WindowInsets.ime
    val isImeVisible by remember {
        derivedStateOf {
            imeInsets.getBottom(density) > 0
        }
    }
    LaunchedEffect(isImeVisible) {
        setSoftKeyboardVisibility(isFocused)
    }