How to clear TextField focus when closing the keyboard and prevent two back presses needed to exit app in Jetpack Compose?
Asked Answered
F

9

22

I'm using BasicTextField.

When I start editing, back button becomes hide keyboard button(arrow down).

First press on back button hides keyboard, but the focus is still on the text field. Both onFocusChanged and BackPressHandler handlers not getting called.

Second press on back button clears focus: onFocusChanged is called and BackPressHandler is not.

BackHandler {
    println("BackPressHandler")
}
val valueState = remember { mutableStateOf(TextFieldValue(text = "")) }
BasicTextField(
    value = valueState.value,
    onValueChange = {
        valueState.value = it
    },
    modifier = Modifier
        .fillMaxWidth()
        .onFocusChanged {
            println("isFocused ${it.isFocused}")
        }
)

Third time BackHandler works fine. Just used it for testing, I shouldn't be needed it here, it expected focus to get lost after first back button tap

Fairyfairyland answered 15/7, 2021 at 7:43 Comment(2)
According to the release notes this is fixed in 1.1.0 jetc.dev/issues/077.htmlTrinetta
@Trinetta from the conversation in compose issue I understand that after the fix second back will close the app, but dismissing the keyboard still won't clear focus, which is still not what I expect in my appFairyfairyland
F
28

There's a compose issue with focused text field prevents back button from dismissing the app when keyboard is hidden. It's marked as fixed, but will be included in some future release, not in 1.0

But, as I understand, the fact that text field is not loosing focus after keyboard being dismissed, is intended behaviour on Android(because of possible connected keyboard? I didn't get the reason). And this is how it works in old android layout too

It seems strange to me, so I came with the following modifier which resigns focus when keyboard disappears:

fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed {
    var isFocused by remember { mutableStateOf(false) }
    var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) }
    if (isFocused) {
        val imeIsVisible = WindowInsets.isImeVisible
        val focusManager = LocalFocusManager.current
        LaunchedEffect(imeIsVisible) {
            if (imeIsVisible) {
                keyboardAppearedSinceLastFocused = true
            } else if (keyboardAppearedSinceLastFocused) {
                focusManager.clearFocus()
            }
        }
    }
    onFocusEvent {
        if (isFocused != it.isFocused) {
            isFocused = it.isFocused
            if (isFocused) {
                keyboardAppearedSinceLastFocused = false
            }
        }
    }
}

Usage:

BasicTextField(
    value = valueState.value,
    onValueChange = {
        valueState.value = it
    },
    modifier = Modifier
        .clearFocusOnKeyboardDismiss()
)
Fairyfairyland answered 17/7, 2021 at 13:19 Comment(8)
Actually, I can't get this to work LocalWindowInsets.current.ime.isVisible always returns false, as I open and close the soft keyboard. At least in the emulator.Trinetta
@Trinetta have you followed WindowCompat.setDecorFitsSystemWindows(window, false) note of accompanist insets?Fairyfairyland
Thanks. And I added ProvideWindowInsets. Now my problem is to layout my screen now it doesn't take into account the status bar and bottom nav bar...Trinetta
@Trinetta if you don't have a plan to interact with them, just add systemBarsPadding for the whole app, something like this. In other case you need to add that padding when needed to each particular screenFairyfairyland
Yes that works, but now when I open the keyboard with a textfield attached to the bottom of the screen, the textfield is now hidden by the keyboard, as I'm using android:windowSoftInputMode="adjustResize. This is the only working solution I can find. But I feel it's a problem with compose and this is overly complex now: remove window insets so you can detect the open and close of a keyboard to unfocus a textfield and you also need to design your app without insets.Trinetta
@Trinetta well as far as I know the roots of this problem goes to the android itself, checking keyboard appearance never was easy =(Fairyfairyland
But when you focus on a textfield in android, it doesn't add to the backstack. As the bug you posted states, the behaviour is not the same as android. Normal android apps don't have this problem.Trinetta
It does not work in the AlertDialogLuthuli
C
19

LATEST UPDATE

This issue has been fixed in the latest versions of Jetpack Compose. So no need to handle it manually.


Old Answer

Thanks for all the answers here. After taking reference from the answers here, here's a solution without using any library

1. Create an extension on View to determine if the keyboard is open or not

fun View.isKeyboardOpen(): Boolean {
    val rect = Rect()
    getWindowVisibleDisplayFrame(rect);
    val screenHeight = rootView.height
    val keypadHeight = screenHeight - rect.bottom;
    return keypadHeight > screenHeight * 0.15
}

2. Create an observable state for determining if a keyboard is open or not

This will listen to Global layout updates on LocalView in which on every event, we check for keyboard open/close status.

@Composable
fun rememberIsKeyboardOpen(): State<Boolean> {
    val view = LocalView.current

    return produceState(initialValue = view.isKeyboardOpen()) {
        val viewTreeObserver = view.viewTreeObserver
        val listener = OnGlobalLayoutListener { value = view.isKeyboardOpen() }
        viewTreeObserver.addOnGlobalLayoutListener(listener)

        awaitDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener)  }
    }
}

3. Create modifier

This modifier will take care of clearing focus on keyboard visible/invisible events.

fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed {

    var isFocused by remember { mutableStateOf(false) }
    var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) }

    if (isFocused) {
        val isKeyboardOpen by rememberIsKeyboardOpen()

        val focusManager = LocalFocusManager.current
        LaunchedEffect(isKeyboardOpen) {
            if (isKeyboardOpen) {
                keyboardAppearedSinceLastFocused = true
            } else if (keyboardAppearedSinceLastFocused) {
                focusManager.clearFocus()
            }
        }
    }
    onFocusEvent {
        if (isFocused != it.isFocused) {
            isFocused = it.isFocused
            if (isFocused) {
                keyboardAppearedSinceLastFocused = false
            }
        }
    }
}

4. Use it

Finally, use it with TextField composable

BasicTextField(Modifier.clearFocusOnKeyboardDismiss())
Carcinoma answered 28/12, 2021 at 11:20 Comment(0)
C
4

If you want to clear the focus of the TextField on hiding the keyboard, it can be done with the combination of LocalFocusManager, keyboardOptions and keyboardActions as shown below.

val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current

TextField(value = "Hello World",
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
            keyboardActions = KeyboardActions(onDone = {
                keyboardController?.hide()
                focusManager.clearFocus()
            }))
Cincinnatus answered 7/9, 2022 at 6:19 Comment(2)
this is not gonna work if keyboard is hidden with system back buttonFairyfairyland
@PhilDukhov Sorry, I misunderstood the question and totally forgot the case you mentioned :(Cincinnatus
T
3

I found an arguably simpler solution using Android's tree observer.

You don't need to use another library or remove the insets from your layout.

It clears the focus in compose any time the keyboard is hidden.

Hopefully this will not be need when this is released.

class MainActivity : ComponentActivity() {

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

  override fun onCreate(state: Bundle?) {
    super.onCreate(state)
    setContent {
      val focusManager = LocalFocusManager.current
      kbClosed = {
        focusManager.clearFocus()
      }
      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.
        kbClosed = false
        // kb opened
      } else if(!kbClosed) {
        kbClosed = true
        kbClosed()
      }
    }
  }
}
Trinetta answered 19/8, 2021 at 13:54 Comment(0)
L
3

@mmm111mmm, only your approach worked for me. I would like to suggest a clean way to encapsulate it.

  1. Create this Composable :
@Composable
fun AppKeyboardFocusManager() {
    val context = LocalContext.current
    val focusManager = LocalFocusManager.current
    DisposableEffect(key1 = context) {
        val keyboardManager = KeyBoardManager(context)
        keyboardManager.attachKeyboardDismissListener {
            focusManager.clearFocus()
        }
        onDispose {
            keyboardManager.release()
        }
    }
}
  1. Use this Composable at call site once on Application level
setContent {
        AppKeyboardFocusManager()
        YouAppMaterialTheme {
          ...
        }
    }
  1. Create Manager with @mmm111mmm approach
/***
 * Compose issue to be fixed in alpha 1.03
 * track from here : https://issuetracker.google.com/issues/192433071?pli=1
 * current work around
 */
class KeyBoardManager(context: Context) {

    private val activity = context as Activity
    private var keyboardDismissListener: KeyboardDismissListener? = null

    private abstract class KeyboardDismissListener(
        private val rootView: View,
        private val onKeyboardDismiss: () -> Unit
    ) : ViewTreeObserver.OnGlobalLayoutListener {
        private var isKeyboardClosed: Boolean = false
        override fun onGlobalLayout() {
            val r = Rect()
            rootView.getWindowVisibleDisplayFrame(r)
            val screenHeight = rootView.rootView.height
            val keypadHeight = screenHeight - r.bottom
            if (keypadHeight > screenHeight * 0.15) {
                // 0.15 ratio is right enough to determine keypad height.
                isKeyboardClosed = false
            } else if (!isKeyboardClosed) {
                isKeyboardClosed = true
                onKeyboardDismiss.invoke()
            }
        }
    }

    fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) {
        val rootView = activity.findViewById<View>(android.R.id.content)
        keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {}
        keyboardDismissListener?.let {
            rootView.viewTreeObserver.addOnGlobalLayoutListener(it)
        }
    }

    fun release() {
        val rootView = activity.findViewById<View>(android.R.id.content)
        keyboardDismissListener?.let {
            rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
        }
        keyboardDismissListener = null
    }
}
Lactobacillus answered 26/9, 2021 at 7:27 Comment(0)
A
1

For me the accepted answer worked, BUT with a small adjustment. I've replaced the deprecated LocalWindowInsets with WindowInsets and worked as a charm.

So here is the code that worked for me - to release the focus when the keyboard gets hidden:

fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed {
    var isFocused by remember { mutableStateOf(false) }
    var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) }
    if (isFocused) {
        val imeIsVisible = WindowInsets.isImeVisible
        val focusManager = LocalFocusManager.current
        LaunchedEffect(imeIsVisible) {
            if (imeIsVisible) {
                keyboardAppearedSinceLastFocused = true
            } else if (keyboardAppearedSinceLastFocused) {
                focusManager.clearFocus()
            }
        }
    }
    onFocusEvent {
        if (isFocused != it.isFocused) {
            isFocused = it.isFocused
            if (isFocused) {
                keyboardAppearedSinceLastFocused = false
            }
        }
    }
}

And for using it just call the clearFocusOnKeyboardDismiss() on your modifier, like so:

 ..
 modifier = Modifier
                .clearFocusOnKeyboardDismiss()
 ..

Hope this will help someone with the same problem, cheers!

Astigmatism answered 3/11, 2022 at 11:15 Comment(0)
S
0

I believe you can manage the focus of your TextField when back button is clicked and ime is hidden using the FocusManager without needing to opt for experimental APIs.

@Composable 
private fun CustomTextField(
    value: String,
    onValueUpdate: (String) -> Unit, 
) {
    val focusManager = LocalFocusManager.current
    val imeState = rememberImeState()
    
    LaunchedEffect(imeState.value) {
        if(!imeState.value) focusManager.clearFocus()
    }
    // Add TextField, OutlinedTextField or BasicTextField here with value and onValueChange parameters.
}
Sulfapyridine answered 6/6, 2023 at 17:38 Comment(1)
What is "rememberImeState"?Zavras
U
0

This issue was fixed in latest version of compose. update your compose version in build gradle

Ungovernable answered 19/2 at 13:56 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Rees
J
-1

In a class that inherits from Application, add the following code to detect when the main activity gets created and include the code that detects when the keyboard is shown or hidden:

import android.app.Activity
import android.app.Application
import android.content.res.Resources
import android.graphics.Rect
import android.os.Bundle
import android.util.DisplayMetrics
import androidx.compose.runtime.mutableStateOf

class App : Application() {

    private val activityLifecycleTracker: AppLifecycleTracker = AppLifecycleTracker()

    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(activityLifecycleTracker)
    }

    companion object {
        val onKeyboardClosed = mutableStateOf(false)
    }

    /**
     * Callbacks for handling the lifecycle of activities.
     */
    class AppLifecycleTracker : ActivityLifecycleCallbacks {

        override fun onActivityCreated(activity: Activity, p1: Bundle?) {
            val displayMetrics: DisplayMetrics by lazy { Resources.getSystem().displayMetrics }
            val screenRectPx = displayMetrics.run { Rect(0, 0, widthPixels, heightPixels) }

            // Detect when the keyboard closes.
            activity.window.decorView.viewTreeObserver.addOnGlobalLayoutListener {
                val r = Rect()
                activity.window.decorView.getWindowVisibleDisplayFrame(r)
                val heightDiff: Int = screenRectPx.height() - (r.bottom - r.top)

                onKeyboardClosed.value = (heightDiff <= 100)
            }
        }

        override fun onActivityStarted(activity: Activity) {
        }

        override fun onActivityResumed(activity: Activity) {
        }

        override fun onActivityPaused(p0: Activity) {
        }

        override fun onActivityStopped(activity: Activity) {
        }

        override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
        }

        override fun onActivityDestroyed(p0: Activity) {
        }
    }
}

Add the following Modifier extension:

@Stable
fun Modifier.clearFocusOnKeyboardClose(focusManager: FocusManager): Modifier {
    if (App.onKeyboardClosed.value) {
        focusManager.clearFocus()
    }

    return this
}

In your composable, add a reference to the FocusManager and add the modifier to your TextField:

@Composable
fun MyComposable() {
   val focusManager = LocalFocusManager.current
   
    OutlinedTextField(
                     modifier = Modifier.clearFocusOnKeyboardClose(focusManager = focusManager)
    )
}

The TextField will clear it's focus whenever the keyboard is closed.

Jessie answered 21/11, 2021 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.