官方 Focus 文档偏 API;本文写 键盘链路 与 edge-to-edge 下的Insets 组合战。
配套示例

FocusInsetsLabScreen.kt:ImeAction.Next 切焦点、Modifier.imePadding()。
1. FocusRequester:谁 request、何时合法?
remember { FocusRequester() }与字段 一一对应 ;不要在循环里remember多个 requester 却共享逻辑,读屏会乱。requestFocus()在 尚未 attached 时抛异常:常见写法是LaunchedEffect(Unit) { focusRequester.requestFocus() }放在 已挂载 之后;或用户点击后再请求。- 大表单 :少量固定输入框可用多个
FocusRequester;字段很多时优先用LocalFocusManager.current.moveFocus(FocusDirection.Next),减少 requester 数量和错绑风险。
本示例 :用键盘 Next 从第一框跳到第二框;避免在 onCreate 粗暴抢焦点打断用户。
2. KeyboardOptions / KeyboardActions
ImeAction.Next/Done要与 实际可聚焦项 对齐;onNext里second.requestFocus()是最小演示。- 多行输入 :
ImeAction与singleLine冲突时,键盘右下角行为会怪异,先统一产品语义。 - Done 行为 :
ImeAction.Done通常要配合提交、清焦点或隐藏键盘;只改键盘图标而不处理动作,会让用户觉得按钮失效。
避坑
- 只写
KeyboardActions却 没对应focusRequester:键盘显示「下一步」但无响应。 BasicTextField自己拼装饰:要重复实现 cursor handle / selection / IME 成本,优先TextField/OutlinedTextField除非你真的需要极致定制。
3. WindowInsets 与 imePadding()
Modifier.imePadding():把 键盘高度 转成底部 padding,减轻遮挡;与enableEdgeToEdge()同屏时,常与statusBarsPadding/navigationBarsPadding组合。- 全屏沉浸 + 键盘 :不要期待「一个 padding 解决所有机型」;要准备 Scaffold 内
innerPadding+ 业务区滚动 的组合方案。 - Insets 消费 :
imePadding()等 insets padding modifier 会把对应 insets 作为 padding 应用到当前节点,并影响子树继续看到的剩余 insets。多层叠加时要明确谁消费系统栏、谁处理 IME。 - 可滚动表单 :键盘只是把底部空间让出来,不保证当前焦点字段自动滚到可见区域。长表单可结合
BringIntoViewRequester或滚动容器,在获得焦点时把字段带入视口。
避坑
- 多层
imePadding()嵌套:底部空白叠罗汉。 - WebView / SurfaceView 与 Compose 键盘:焦点竞争 要单独开 issue 级方案,不在此篇假装一次搞定。
4. BringIntoViewRequester:什么时候需要?
imePadding() 解决的是「键盘出现后底部留空间」,BringIntoViewRequester 解决的是「某个输入框被遮住时,把它滚到可见范围」。两者常一起出现,但职责不同。
典型思路:
kotlin
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent { state ->
if (state.isFocused) {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
},
)
生产里要结合表单滚动容器、焦点切换节奏和 IME 动画验证;不要只靠一层 imePadding() 期望所有机型都自然滚对。
5. 自检清单
FocusRequester是否在remember内创建 ,且与「一输入框一 requester」对应,而非在items {}里错误复用?requestFocus()是否在组合 已挂载 之后调用(如LaunchedEffect(Unit)或用户点击后),避免启动即崩?KeyboardActions/ImeAction.Next是否与FocusRequester.requestFocus()或FocusManager.moveFocus实际接通?imePadding()是否与statusBarsPadding/navigationBarsPadding/ ScaffoldinnerPadding搭配验收,且避免 多层imePadding叠罗汉?- 长表单里焦点字段是否需要
BringIntoViewRequester,而不是只靠键盘 padding?
参考答案(复习用)
- 是 。
val first = remember { FocusRequester() }放在固定子树;Lazy item 若需 requester,应用key(itemId)与 item 生命周期对齐,避免复用错绑。 - 应延后 。首帧自动聚焦用
LaunchedEffect(Unit) { focusRequester.requestFocus() };启动瞬间抢焦点易打断用户,除非无障碍/表单强需求。 - 应接通 。仅设
ImeAction.Next而不在onNext/KeyboardActions里second.requestFocus()或focusManager.moveFocus(...),键盘「下一步」无效果;本仓库FocusInsetsLabScreen.kt为最小演示。 - 应组合验收 。edge-to-edge 下通常「Scaffold 消费系统栏 + 业务列
imePadding()」;同一子树重复imePadding()会底部空白过大,应合并到一层 Modifier 链。 - 需要时补 。短表单可只用
imePadding();长表单、底部输入框、聊天输入区等,通常要配合滚动容器与BringIntoViewRequester。
源码仓库 :ComposeDemo(分支
main)