Jetpack Compose 焦点与键盘:FocusRequester、imePadding 与 BringIntoView 实战

官方 Focus 文档偏 API;本文写 键盘链路edge-to-edge 下的Insets 组合战

配套示例

FocusInsetsLabScreen.ktImeAction.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 要与 实际可聚焦项 对齐;onNextsecond.requestFocus() 是最小演示。
  • 多行输入ImeActionsingleLine 冲突时,键盘右下角行为会怪异,先统一产品语义。
  • Done 行为ImeAction.Done 通常要配合提交、清焦点或隐藏键盘;只改键盘图标而不处理动作,会让用户觉得按钮失效。

避坑

  • 只写 KeyboardActions没对应 focusRequester:键盘显示「下一步」但无响应。
  • BasicTextField 自己拼装饰:要重复实现 cursor handle / selection / IME 成本,优先 TextField/OutlinedTextField 除非你真的需要极致定制。

3. WindowInsetsimePadding()

  • 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. 自检清单

  1. FocusRequester 是否在 remember 内创建 ,且与「一输入框一 requester」对应,而非在 items {} 里错误复用?
  2. requestFocus() 是否在组合 已挂载 之后调用(如 LaunchedEffect(Unit) 或用户点击后),避免启动即崩?
  3. KeyboardActions / ImeAction.Next 是否与 FocusRequester.requestFocus()FocusManager.moveFocus 实际接通?
  4. imePadding() 是否与 statusBarsPadding / navigationBarsPadding / Scaffold innerPadding 搭配验收,且避免 多层 imePadding 叠罗汉
  5. 长表单里焦点字段是否需要 BringIntoViewRequester,而不是只靠键盘 padding?

参考答案(复习用)

  1. val first = remember { FocusRequester() } 放在固定子树;Lazy item 若需 requester,应用 key(itemId) 与 item 生命周期对齐,避免复用错绑。
  2. 应延后 。首帧自动聚焦用 LaunchedEffect(Unit) { focusRequester.requestFocus() };启动瞬间抢焦点易打断用户,除非无障碍/表单强需求。
  3. 应接通 。仅设 ImeAction.Next 而不在 onNext / KeyboardActionssecond.requestFocus()focusManager.moveFocus(...),键盘「下一步」无效果;本仓库 FocusInsetsLabScreen.kt 为最小演示。
  4. 应组合验收 。edge-to-edge 下通常「Scaffold 消费系统栏 + 业务列 imePadding()」;同一子树重复 imePadding() 会底部空白过大,应合并到一层 Modifier 链。
  5. 需要时补 。短表单可只用 imePadding();长表单、底部输入框、聊天输入区等,通常要配合滚动容器与 BringIntoViewRequester

源码仓库ComposeDemo(分支 main

相关推荐

《Measure、Place 与 Constraints 约束传递》

《Material3 组件选择、状态管理与避坑指南》

相关推荐
三少爷的鞋1 分钟前
Android 架构演进:从生命周期事件到结构化任务
android
一笑的小酒馆7 小时前
AndroidKMP之网络请求
android
aqi0010 小时前
鸿蒙版本的JSBridge兼容与安卓配套的H5啦
android·华为·harmonyos·鸿蒙·移动应用
事圆则缓11 小时前
MVVM 的理解与设计
android·android jetpack
码农阿豪12 小时前
我把 OpenClaw 装进旧安卓手机后,又折腾了 3 天:飞书、ADB、SSH 全部打通
android·智能手机·飞书
有什么事14 小时前
把安卓搬上云端:云手机系统改造与内核优化拆解
android·智能手机
方白羽14 小时前
性能分析:Android Studio Profiler
android·性能优化·app
没文化的阿浩14 小时前
【MySQL】用户管理
android·mysql·adb
FlightYe15 小时前
音视频修炼之基础理论(五):AAC格式与解析
android·linux·c++·音视频·aac
法欧特斯卡雷特16 小时前
Kotlin 2.4.20 现已发布,新特性多不多?
android·开源·全栈