如何在关闭键盘时清除 TextField 焦点并防止在 Jetpack Compose 中退出应用程序所需的两次后退?

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

我正在使用 BasicTextField。

当我开始编辑时,后退按钮变成隐藏键盘按钮(向下箭头)。

首先按下后退按钮隐藏键盘,但焦点仍在文本字段上。 onFocusChangedBackPressHandler 处理程序都没有被调用。

第二次按下后退按钮清除焦点:调用 onFocusChangedBackPressHandler 未调用。

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

第三次 BackHandler 工作正常。只是用它来测试,我不应该在这里需要它,它预计在第一次后退按钮点击后焦点会丢失

有一个 compose issue 带有焦点文本字段,可防止后退按钮在隐藏键盘时关闭应用程序。它被标记为已修复,但会包含在未来的某个版本中,而不是 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

安装 accompanist insets 依赖项

p.s.s. Since Compose 1.2.0-alpha03, Accompanist Insets was mostly moved into Compose Foundation, check out migration guide 了解更多详情。 LocalWindowInsets.current.ime 应替换为 WindowInsets.ime


用法:

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

我使用 Android 的树观察器找到了一个可以说更简单的解决方案。

您不需要使用其他库或从布局中删除插图。

只要隐藏键盘,它就会清除撰写中的焦点。

希望 this 发布时不需要这个。

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,只有你的方法对我有用。我想建议一个干净的方法来封装它。

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

在继承自 Application 的 class 中,添加以下代码以检测主 activity 何时创建,并包含检测键盘何时显示或隐藏的代码:

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 就会清除它的焦点。

感谢这里的所有回答。参考这里的答案后,这里有一个不使用任何库的解决方案

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 上的全局布局更新,其中在每个事件中,我们都会检查键盘 open/close 状态。

@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。创建修改器

此修改器将负责清除键盘 visible/invisible 事件的焦点。

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