Jetpack Compose禁用输入框软键盘

493 阅读3分钟

在一些业务中,我们不希望在编辑框中弹出软键盘,比如计算器、拨号键盘。但是在Compose的BasicTextField中,没有可配置软键盘的选项。

如果配置了readOnly=true,虽然也不会弹出软键盘,但是编辑框的操作也被禁用了,无法选中内容、跳转指针。

对比Android View系统的AppCompatEditText,在EditText中只需要设置showSoftInputOnFocus = false,即可禁用弹出软键盘

探索方法

在寻找网上和AI给出的方案中,基本是通过监听焦点变化,手动隐藏软键盘。实现大概如下:

@Composable
fun DisableKeyboardTextField() {
    val keyboardController = LocalSoftwareKeyboardController.current
    var text by remember { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = { text = it },
        modifier = Modifier
            .onFocusEvent { focusState ->
                if (focusState.isFocused) {
                    keyboardController?.hide()
                }
            }
    )
}

但实际中,这样设置并不生效。打印日志发现TextField的focusState一直是true,没有其他的控件发生焦点转移,无论点击哪里都不会失去焦点。

网上求助无果,于是只能通过分析源码来找到解决思路。

BasicTextField实现

BasicTextField的核心实现是CoreTextField,在CoreTextField中看到输入相关实现:

val legacyTextInputServiceAdapter = remember { createLegacyPlatformTextInputServiceAdapter() }
val textInputService: TextInputService = remember {
    TextInputService(legacyTextInputServiceAdapter)
}

其中legacyTextInputServiceAdapter管理键盘的地方如下:

final override fun showSoftwareKeyboard() {
    textInputModifierNode?.softwareKeyboardController?.show()
}

final override fun hideSoftwareKeyboard() {
    textInputModifierNode?.softwareKeyboardController?.hide()
}

而其中LegacyPlatformTextInputNode的实现为LegacyAdaptingPlatformTextInputModifierNode,这个东西继承自PlatformTextInputModifierNode。

internal class LegacyAdaptingPlatformTextInputModifierNode(
    private var serviceAdapter: LegacyPlatformTextInputServiceAdapter,
    override var legacyTextFieldState: LegacyTextFieldState,
    override var textFieldSelectionManager: TextFieldSelectionManager
) : Modifier.Node(),
    PlatformTextInputModifierNode,
    CompositionLocalConsumerModifierNode,
    GlobalPositionAwareModifierNode,
    LegacyPlatformTextInputServiceAdapter.LegacyPlatformTextInputNode

在PlatformTextInputModifierNode中,存在一个PlatformTextInputInterceptor的接口,这个接口是一个ExperimentalComposeUiApi 实验性API

@ExperimentalComposeUiApi
fun interface PlatformTextInputInterceptor {

    /**
     * Called when a function passed to
     * [establishTextInputSession][PlatformTextInputModifierNode.establishTextInputSession] calls
     * [startInputMethod][PlatformTextInputSession.startInputMethod]. The
     * [PlatformTextInputMethodRequest] from the caller is passed to this function as [request], and
     * this function can either respond to the request directly (e.g. recording it for a test), or
     * wrap the request and pass it to [nextHandler]. This function _must_ call into [nextHandler]
     * if it intends for the system to respond to the request. Not calling into [nextHandler] has
     * the effect of blocking the request.
     *
     * This method has the same ordering guarantees as
     * [startInputMethod][PlatformTextInputSession.startInputMethod]. That is, for a given text
     * input modifier, if [startInputMethod][PlatformTextInputSession.startInputMethod] is called
     * multiple times, only one [interceptStartInputMethod] call will be made at a time, and any
     * previous call will be allowed to finish running any `finally` blocks before the new session
     * starts.
     */
    suspend fun interceptStartInputMethod(
        request: PlatformTextInputMethodRequest,
        nextHandler: PlatformTextInputSession
    ): Nothing
}

这个PlatformTextInputInterceptor是在建立输入会话时候的一个拦截器,当点击输入框到弹出软键盘这一过程,就需要经过PlatformTextInputInterceptor的拦截。在拦截器中,如果需要响应输入框的键盘输入请求,则需要调用nextHandler. startInputMethod(request)方法。如果不需要系统键盘输入,则可以拦截这个请求。

在注释中,谷歌人员给出了一个使用示例,来选择禁用软键盘弹出:

@Composable
fun DisableSoftKeyboard(disable: Boolean = true, content: @Composable () -> Unit) {
    InterceptPlatformTextInput(
        interceptor = { request, nextHandler ->
            // If this flag is changed while an input session is active, a new lambda instance
            // that captures the new value will be passed to InterceptPlatformTextInput, which
            // will automatically cancel the session upstream and restart it with this new
            // interceptor.
            if (!disable) {
                // Forward the request to the system.
                nextHandler.startInputMethod(request)
            } else {
                // This function has to return Nothing, and since we don't have any work to do
                // in this case, we just suspend until cancelled.
                awaitCancellation()
            }
        },
        content = content
    )
}

禁用输入框软键盘

总结一下,禁用输入框的代码大致如下:

    InterceptPlatformTextInput(
        interceptor = { _, _ ->
            awaitCancellation()
        },
        content = {
            BasicTextField(
                value = "text",
                onValueChange = {}

            )
        }
    )

利用InterceptPlatformTextInput包了一层,Content里的输入框就不会再弹出软键盘了