Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 554
Philip Dukhov Avatar asked Jul 15 '21 07:07

Philip Dukhov


People also ask

What is textfield in jetpack compose?

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.

How to hide or close the keyboard when the textfield is focused?

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.

How do I clear the focus of a textfield?

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.

Why should I clear the textfield after submitting the form?

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.


5 Answers

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()
)
like image 161
Philip Dukhov Avatar answered Nov 09 '22 18:11

Philip Dukhov


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())
like image 44
Shreyas Patil Avatar answered Nov 09 '22 19:11

Shreyas Patil


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()
      }
    }
  }
}
like image 34
mmm111mmm Avatar answered Nov 09 '22 18:11

mmm111mmm


@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
    }
}
like image 37
Chetan Gupta Avatar answered Nov 09 '22 18:11

Chetan Gupta


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.

like image 44
Johann Avatar answered Nov 09 '22 19:11

Johann