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
In Jetpack Compose, a TextField is a UI element that lets users type in text as an input. This input can then be stored and used for various desired functions. When a user clicks the TextField, a soft keyboard pops up from the bottom and the user can type in the desired text.
You can Simply achieve this by using GestureDetector widget, which makes it simple and easiest way to detect onTap outside the focused TextField. To close keyboard / to hide the keyboard you need to simply remove focus from textfield & thus it automatically dismiss the keyboard.
When a user clicks the TextField, a soft keyboard pops up from the bottom and the user can type in the desired text. The TextField cursor keeps blinking unless the back button is clicked or the screen is clicked outside the TextField. This is called clearing focus from TextField.
Suppose, you are creating a form and after submitting you are redirected to a new page, if you go back now then the entered text would still be there and this is not a sign of a good UI. So, in this scenario, you would be required to clear the TextField after submitting.
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 = LocalWindowInsets.current.ime.isVisible
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
}
}
}
}
p.s. You need to install accompanist insets dependency for LocalWindowInsets.current.ime
p.s.s. Since Compose 1.2.0-alpha03, Accompanist Insets was mostly moved into Compose Foundation, check out migration guide for more details. LocalWindowInsets.current.ime
should be replaced with WindowInsets.ime
.
Usage:
BasicTextField(
value = valueState.value,
onValueChange = {
valueState.value = it
},
modifier = Modifier
.clearFocusOnKeyboardDismiss()
)
Thanks to 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())
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()
}
}
}
}
@mmm111mmm, only your approach worked for me. I would like to suggest a clean way to encapsulate it.
@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()
}
}
}
setContent {
AppKeyboardFocusManager()
YouAppMaterialTheme {
...
}
}
/***
* 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
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With