3. Jetpack Compose 输入组件基础:TextField 与 BasicTextField 架构、InputConnection、CompositionLocal

3.1 本章概述

在构建防截屏输入组件时,深入理解 Compose 的输入层是必要的。TextField 与 BasicTextField 是 Compose Material3 中两个核心输入组件,它们基于相同的底层架构,但对外暴露的抽象程度不同。本章将剖析其架构层次、关键接口 InputConnection 以及组件上下文传递机制 CompositionLocal,为后续实现安全输入控件打下基础。

3.2 TextField 与 BasicTextField 对比

维度TextField (Material3)BasicTextField
设计定位开箱即用的 Material3 风格输入框,自带标签、指示器、错误状态无样式、功能精简的基础输入组件,适合自定义
内部组件集成了 OutlinedTextField 或 FilledTextField 的外观层仅包含 CoreTextField 和 BasicTextField2 内部实现
参数复杂度大量样式与行为参数(约 30+)仅保留核心输入参数(约 15 个)
自定义灵活性受限于 Material 规范,修改底层行为困难完全可控,可直接管理 TextInputService 和 InputConnection
性能开销较重组(含动画、状态管理)轻量,仅处理输入事件
适用场景标准表单、符合 Material Design 的场景自定义输入样式、安全输入、特殊字符过滤

3.3 底层架构:CoreTextField 与 InputConnection

无论是 TextField 还是 BasicTextField,最终都会委托给 CoreTextField 组件。CoreTextField 维护一个 TextInputService 实例,该实例负责与平台输入法(IME)通信。通信的核心接口是 InputConnection。

  • TextInputService:Compose 输入系统的服务注册器,管理输入连接和会话。
  • InputConnection:用于与输入法交换文本、组合键、光标位置等信息的桥梁。
  • TextLayoutResult:提供文本布局信息(光标坐标、行高),输入法通过它定位光标。
关键点:防截屏输入组件需要重写 InputConnection 的 commitText() 和 setComposingText() 方法,以阻断通过 IME 截屏(如输入法自带截屏或系统截屏接口)对输入内容的读取。

3.4 CompositionLocal 在输入组件中的作用

CompositionLocal 是 Compose 中隐式传递上下文数据的机制。在输入组件中,以下 CompositionLocal 至关重要:

CompositionLocal类型默认提供者用途
LocalTextInputServiceTextInputServiceTextInputService(null)为子树提供输入服务实例
LocalTextToolbarTextToolbar空实现提供文本选择工具栏(复制/粘贴等)
LocalClipboardManagerClipboardManager平台默认实现访问系统剪贴板

在自定义 BasicTextField 时,必须确保 LocalTextInputService 被正确提供,否则输入组件将无法响应键盘事件。通常由 CompositionLocalProvider 包裹:

CompositionLocalProvider(LocalTextInputService provides myInputService) {
    BasicTextField(...)
}

3.5 架构图:输入组件内部协作流程

TextField / BasicTextField 输入架构协作图 Compose 输入子系统 用户触摸/键盘事件 CoreTextField TextInputService InputConnection 平台输入法 IME TextLayoutResult 说明:虚线表示 CompositionLocal 隐式传递;实线箭头表示显式调用/事件流

3.6 关键 API 与代码示例

3.6.1 使用 BasicTextField 自定义输入

@Composable
fun MyInputField(value: String, onValueChange: (String) -> Unit) {
    BasicTextField(
        value = value,
        onValueChange = onValueChange,
        modifier = Modifier.fillMaxWidth().padding(8.dp),
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
        singleLine = true
    )
}

3.6.2 通过 CompositionLocal 注入自定义 TextInputService

val customInputService = remember {
    object : TextInputService(null) {
        override fun createInputConnection(): InputConnection {
            // 返回自定义 InputConnection 实现
            return CustomSecureInputConnection()
        }
    }
}

CompositionLocalProvider(LocalTextInputService provides customInputService) {
    BasicTextField(value = text, onValueChange = { text = it })
}

3.6.3 获取 InputConnection 并监听输入事件

class SecureInputConnection(target: InputConnection) : InputConnectionWrapper(target) {
    override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
        // 防截屏:检查是否是截屏触发的 commitText
        if (text.startsWith("SCREENSHOT_MAGIC")) {
            return false // 拒绝
        }
        return super.commitText(text, newCursorPosition)
    }
}

3.7 调试技巧

  • 启用输入日志:在 AndroidManifest.xml 中设置 android:debuggable="true",并在 InputConnection 方法内添加 Log.d("Input", "commitText: $text") 观察输入法行为。
  • 使用 Layout Inspector:检查 Compose 组件树中 LocalTextInputService 是否被正确覆盖。
  • 模拟截屏事件:通过 ADB 命令 adb shell input tap x y 触发长按或组合键,观察 commitText 是否被阻断。
常见错误:在未提供 LocalTextInputService 的情况下直接使用 BasicTextField,会导致无法弹出键盘。错误日志为 No TextInputService available。解决方案:在组件树顶层或 TextField 内部使用 CompositionLocalProvider 提供自定义实例。

3.8 性能与稳定性优化

优化目标方法说明
减少重组将 value 和 onValueChange 拆分为 remember 状态,避免无意义的 lambda 重建使用 derivedStateOf 或 mutableStateOf 时注意作用域
InputConnection 复用在 remember 中创建一次 InputConnectionWrapper,避免每次输入都新建对象减少 GC 压力
CompositionLocal 范围将 CompositionLocalProvider 包裹在最小的组件子树内,不要包裹整个页面避免不必要的全局传递
防截屏判断在 commitText 中使用 requireNotNull 防止空指针;对平台版本做兼容(Android 12+ 有额外限制)使用 Build.VERSION.SDK_INT 做条件分支

3.9 本章小结