Как очистить фокус TextField при закрытии клавиатуры в Jetpack Compose?
Я использую OutlinesTextField.
Когда я начинаю редактировать, кнопка возврата становится кнопкой скрытия клавиатуры (стрелка вниз).
Первое нажатие на кнопку «Назад» скрывает клавиатуру, но фокус по-прежнему находится в текстовом поле. Оба оператора и обработчики не получают вызова.
Второе нажатие кнопки возврата очищает фокус:
onFocusChanged
называется и
BackPressHandler
не является.
BackPressHandler {
println("BackPressHandler")
}
val valueState = remember { mutableStateOf(TextFieldValue(text = "")) }
OutlinedTextField(
value = valueState.value,
onValueChange = {
valueState.value = it
},
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
println("isFocused ${it.isFocused}")
}
)
Третий раз BackHandler работает нормально, брал если из compose-samples. Просто использовал его для тестирования, он мне здесь не нужен, он ожидал, что фокус потеряется после первого нажатия кнопки возврата
8 ответов
Существует проблема создания с сфокусированным текстовым полем, которое не позволяет кнопке «Назад» закрыть приложение, когда клавиатура скрыта. Он отмечен как исправленный, но будет включен в некоторые будущие выпуски, а не в
1.0
Но, насколько я понимаю, тот факт, что текстовое поле не теряет фокус после закрытия клавиатуры, является предполагаемым поведением на Android(из-за возможного подключения клавиатуры? Я не понял причины). И вот как это работает и в старом макете Android
Мне это кажется странным, поэтому я пришел со следующим модификатором, который меняет фокус при исчезновении клавиатуры:
fun Modifier.fixKeyboardFocusIssue(): Modifier = composed {
var isFocused by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
if (isFocused) {
// TODO: replace with single if when https://issuetracker.google.com/issues/193907134 fixed
if (!LocalWindowInsets.current.ime.isVisible) {
LocalFocusManager.current.clearFocus()
}
}
onFocusChanged {
if (it.isFocused && !isFocused) {
scope.launch {
// wait until keyboard presented on start editing
delay(300)
isFocused = it.isFocused
}
} else {
isFocused = it.isFocused
}
}
}
Использование:
BasicTextField(
value = valueState.value,
onValueChange = {
valueState.value = it
},
modifier = Modifier
.fixKeyboardFocusIssue()
)
Спасибо всем ответам здесь. Взяв ссылку из ответов здесь, вот решение без использования какой-либо библиотеки
1. Создайте расширение на View, чтобы определить, открыта клавиатура или нет.
fun View.isKeyboardOpen(): Boolean {
val rect = Rect()
getWindowVisibleDisplayFrame(rect);
val screenHeight = rootView.height
val keypadHeight = screenHeight - rect.bottom;
return keypadHeight > screenHeight * 0.15
}
2. Создайте наблюдаемое состояние для определения, открыта клавиатура или нет.
Это будет прослушивать глобальные обновления макета в LocalView, в которых при каждом событии мы проверяем статус открытия/закрытия клавиатуры.
@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. Создать модификатор
Этот модификатор позаботится об очистке фокуса от видимых/невидимых событий клавиатуры.
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. Используйте это
Наконец, используйте его с
TextField
компонуемый
BasicTextField(Modifier.clearFocusOnKeyboardDismiss())
Я нашел, возможно, более простое решение, используя обозреватель деревьев Android.
Вам не нужно использовать другую библиотеку или удалять вставки из макета.
Он сбрасывает фокус в композиции каждый раз, когда клавиатура скрыта.
Надеюсь, когда он будет выпущен, в этом не будет необходимости .
class MainActivity : ComponentActivity() {
var kbClosed: () -> Unit = {}
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.
// kb opened
} else {
kbClosed()
}
}
}
}
@mmm111mmm, у меня сработал только ваш подход. Я хотел бы предложить чистый способ инкапсулировать его.
- Создайте этот составной объект:
@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()
}
}
}
- Используйте этот компонент Composable на сайте вызова один раз на уровне приложения
setContent {
AppKeyboardFocusManager()
YouAppMaterialTheme {
...
}
}
- Создать диспетчер с подходом @ mmm111mmm
/***
* 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
}
}
Если вы хотите убрать фокусTextField
чтобы скрыть клавиатуру, это можно сделать комбинациейLocalFocusManager
,keyboardOptions
иkeyboardActions
как показано ниже.
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
TextField(value = "Hello World",
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide()
focusManager.clearFocus()
}))
Для меня сработал принятый ответ, НО с небольшой поправкой. Я заменил устаревшееLocalWindowInsets
сWindowInsets
и работал как оберег.
Итак, вот код, который у меня сработал - чтобы отпустить фокус, когда клавиатура скрывается:
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
}
}
}
}
И для его использования просто позвонитеclearFocusOnKeyboardDismiss()
на вашем модификаторе, например:
..
modifier = Modifier
.clearFocusOnKeyboardDismiss()
..
Надеюсь, это поможет кому-то с той же проблемой, ура!
В класс, наследуемый от Application, добавьте следующий код, чтобы определить, когда создается основное действие, и включите код, который определяет, отображается или скрывается клавиатура:
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) {
}
}
}
Добавьте следующее расширение модификатора:
@Stable
fun Modifier.clearFocusOnKeyboardClose(focusManager: FocusManager): Modifier {
if (App.onKeyboardClosed.value) {
focusManager.clearFocus()
}
return this
}
В компоновке добавьте ссылку на FocusManager и добавьте модификатор в свой TextField:
@Composable
fun MyComposable() {
val focusManager = LocalFocusManager.current
OutlinedTextField(
modifier = Modifier.clearFocusOnKeyboardClose(focusManager = focusManager)
)
}
TextField будет очищать фокус всякий раз, когда клавиатура закрывается.
Я считаю, что вы можете управлять фокусом вашего TextField при нажатии кнопки «Назад» и скрытии времени с помощью FocusManager без необходимости выбирать экспериментальные API.
@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.
}