Как очистить фокус текстового поля при закрытии клавиатуры и предотвратить два нажатия назад, необходимые для выхода из приложения в Jetpack Compose?

#android #android-jetpack-compose

Вопрос:

Я использую базовое текстовое поле.

Когда я начинаю редактирование, кнопка «Назад» становится кнопкой «Скрыть клавиатуру» (стрелка вниз).

Первое нажатие кнопки «Назад» скрывает клавиатуру, но фокус по-прежнему сосредоточен на текстовом поле. И onFocusChanged то, и другое, и BackPressHandler обработчики не вызываются.

Второе нажатие на кнопку «Назад» очищает фокус: onFocusChanged вызывается и BackPressHandler не вызывается.

 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}")
        }
)
 

В третий раз бэкхендлер работает нормально. Просто использовал его для тестирования, он мне здесь не нужен, он ожидал, что фокус потеряется после первого нажатия кнопки «Назад».

Комментарии:

1. Согласно примечаниям к выпуску, это исправлено в версии 1.1.0 jetc.dev/issues/077.html

2. @mmm111mmm из разговора в выпуске «сочинение » я понимаю, что после исправления вторая обратная сторона закроет приложение, но отключение клавиатуры все равно не очистит фокус, что все еще не то, что я ожидаю в своем приложении

Ответ №1:

Существует проблема с составлением сфокусированного текстового поля, из-за которой кнопка «Назад» не позволяет закрыть приложение, когда клавиатура скрыта. Он помечен как исправленный, но будет включен в какой-нибудь будущий выпуск, а не в 1.0

Но, как я понимаю, тот факт, что текстовое поле не теряет фокус после отключения клавиатуры, является предполагаемым поведением на Android(из-за возможной подключенной клавиатуры? Я не понял причины). И вот как это работает и в старом макете Android

Мне это кажется странным, поэтому я пришел со следующим модификатором, который изменяет фокус, когда клавиатура исчезает:

 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. Вам необходимо установить зависимость «Вставки аккомпаниатора» для LocalWindowInsets.current.ime

Использование:

 BasicTextField(
    value = valueState.value,
    onValueChange = {
        valueState.value = it
    },
    modifier = Modifier
        .clearFocusOnKeyboardDismiss()
)
 

Комментарии:

1. На самом деле, я не могу заставить это работать LocalWindowInsets.current.ime.isVisible , всегда возвращает false, когда я открываю и закрываю программную клавиатуру. По крайней мере, в эмуляторе.

2. @mmm111mmm вы следили WindowCompat.setDecorFitsSystemWindows(window, false) за примечанием accompanist insets ?

3. Спасибо. И я добавил ProvideWindowInsets . Теперь моя проблема в том, чтобы расположить мой экран, теперь он не учитывает строку состояния и нижнюю панель навигации…

4. @mmm111mmm если у вас нет плана взаимодействия с ними, просто добавьте systemBarsPadding для всего приложения что-то вроде этого . В другом случае вам нужно добавить это дополнение, когда это необходимо, к каждому конкретному экрану

5. Да, это работает, но теперь, когда я открываю клавиатуру с текстовым полем, прикрепленным к нижней части экрана, текстовое поле теперь скрыто клавиатурой, как я использую android:windowSoftInputMode="adjustResize . Это единственное рабочее решение, которое я могу найти. Но я чувствую, что это проблема с составлением, и сейчас это слишком сложно: удалите вставки окон, чтобы вы могли обнаруживать открытие и закрытие клавиатуры, чтобы расфокусировать текстовое поле, а также вам нужно разработать приложение без вставок.

Ответ №2:

Я нашел, возможно, более простое решение, используя древовидный наблюдатель Android.

Вам не нужно использовать другую библиотеку или удалять вставки из макета.

Он очищает фокус при создании в любое время, когда клавиатура скрыта.

Надеюсь, это не понадобится, когда это будет выпущено.

 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()
      }
    }
  }
}
 

Ответ №3:

@ммм111ммм, только твой подход сработал для меня. Я хотел бы предложить чистый способ его инкапсуляции.

  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. Используйте это составное устройство на сайте вызова один раз на уровне приложения
 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
    }
}
 

Ответ №4:

Спасибо за все ответы здесь. Взяв ссылку на ответы здесь, вот решение без использования какой-либо библиотеки

1. Создайте расширение в режиме просмотра, чтобы определить, открыта клавиатура или нет

 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())
 

Ответ №5:

В классе, который наследуется от приложения, добавьте следующий код, чтобы определить, когда создается основное действие, и включите код, который определяет, когда клавиатура отображается или скрыта:

 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 и добавьте модификатор в текстовое поле:

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

Текстовое поле будет очищаться от фокуса всякий раз, когда клавиатура закрыта.