Как очистить фокус 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, у меня сработал только ваш подход. Я хотел бы предложить чистый способ инкапсулировать его.

  1. Создайте этот составной объект:
      @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. Используйте этот компонент Composable на сайте вызова один раз на уровне приложения
      setContent {
        AppKeyboardFocusManager()
        YouAppMaterialTheme {
          ...
        }
    }
  1. Создать диспетчер с подходом @ 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.
}
Другие вопросы по тегам