Jetpack Compose-收起键盘及键盘防遮挡

前言

文本的输入框架本身提供了

  • BasicTextField

  • TextField

  • OutlinedTextField

BasicTextField是最基本的输入框,没有什么样式,也方便我们自定义,是我们最常用的组件。

后两者有自带的样式和交互效果,但是实际项目中并不符合我们的效果,所以一般很少用。

使用过程中我们要注意两个问题

  • 怎样收起键盘?
  • 怎样在键盘显示的时候防止遮挡界面?

收起键盘

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Composable
fun BasicTextFieldWithDismissKeyboard() {
// 获取焦点管理器(用于清除焦点)
val focusManager = LocalFocusManager.current
// 可选:获取键盘控制器(用于强制收起键盘)
val keyboardController = LocalSoftwareKeyboardController.current

// 父布局:点击时清除焦点(收起键盘)
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
// 点击空白区域时清除焦点
.clickable {
focusManager.clearFocus() // 清除所有焦点,键盘会自动收起
}
// 防止点击事件被子组件消费后父组件无法响应
.pointerInput(Unit) { /* 空实现,确保点击事件能冒泡到父组件 */ },
contentAlignment = Alignment.Center
) {
// 输入框
BasicTextField(
value = "",
onValueChange = {},
)
}
}

两种方式

方式1

1
2
3
4
5
// 获取焦点管理器(用于清除焦点)
val focusManager = LocalFocusManager.current

// 清除所有焦点,键盘会自动收起
focusManager.clearFocus()

方式2

1
2
3
4
5
// 可选:获取键盘控制器(用于强制收起键盘)
val keyboardController = LocalSoftwareKeyboardController.current

// 可选:强制收起键盘(某些场景下需要)
keyboardController?.hide()

核心实现原理

  1. 焦点管理:通过 LocalFocusManager 获取焦点管理器,调用 focusManager.clearFocus() 可清除所有组件的焦点,输入框失去焦点后键盘会自动收起。
  2. 父布局可点击:将整个屏幕的父布局(如 Box)设置为 clickable,点击时触发焦点清除操作。
  3. 事件冒泡:通过 pointerInput(Unit) {} 确保点击事件能从子组件传递到父组件(避免子组件消费事件后父组件无法响应)。
  4. 可选:强制收起键盘:通过 LocalSoftwareKeyboardControllerhide() 方法可以强制收起键盘,适合某些特殊场景(如焦点清除后键盘未自动收起的情况)。

注意事项

  • 确保父布局的 clickable 修饰符作用于整个可点击区域(通常是 fillMaxSize() 的容器)。
  • 输入框本身的点击事件不会触发父布局的 clickable(因为事件会被输入框优先消费),这符合预期(点击输入框时应保持焦点并弹出键盘)。
  • 此方案适用于所有需要点击空白区域收起键盘的场景(不仅限于 BasicTextField,也适用于 TextField 等输入组件)。

键盘防遮挡

Jetpack Compose 中,当软键盘(IME)弹出时遮挡输入框(如 TextField)是一个常见问题。

Android 系统本身会尝试滚动或调整窗口,但在 Compose 中需要配合正确的配置和布局策略才能确保输入框始终可见。

三步解决键盘遮挡问题

设置windowSoftInputMode

AndroidManifest.xml 中设置 windowSoftInputMode

这是最关键的一步!Compose 本身无法控制 IME 行为,必须由 Activity 配合。

1
2
3
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
  • adjustResize:Activity 的窗口会被压缩(不是平移),为键盘腾出空间。
  • ❌ 避免使用 adjustPan(它只平移,不压缩,容易导致布局错乱)。

设置imePadding

使用 imePadding() + 可滚动容器

即使设置了 adjustResize,如果内容不可滚动,底部的 TextField 仍可能被键盘盖住。

你需要:

  1. 包裹内容在可滚动容器中(如 LazyColumnverticalScroll
  2. 在底部添加 imePadding()

注意

如果空白空间足够显示弹窗,可以不用在滚动容器中,只设置Modifier.imePadding()

使用 LazyColumn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Composable
fun ChatScreen() {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding() // 👈 关键:自动添加 IME 高度的 padding
) {
items(20) { index ->
Text("Message $index")
}

// 将 TextField 放在底部
item {
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Type something") },
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding() // 可选:避开导航栏
)
}
}
}

使用 Column + verticalScroll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Composable
fun FormScreen() {
val scrollState = rememberScrollState()

Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.imePadding() // 👈 键盘弹出时底部留空
) {
repeat(10) {
Text("Field $it", modifier = Modifier.padding(16.dp))
}

OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Input") },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}

imePadding() 会自动监听 IME 显示/隐藏,并动态添加 padding.bottom = 键盘高度

自动滚动到输入框(可选)

如果输入框不在可视区域,可以在获得焦点时自动滚动到底部

方法:使用 bringIntoViewRequester

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Composable
fun AutoScrollTextField() {
val scrollState = rememberScrollState()
val bringIntoViewRequester = remember { BringIntoViewRequester() }

Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.imePadding()
) {
Spacer(modifier = Modifier.height(2000.dp)) // 模拟长内容

OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Bottom Input") },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent { focusState ->
if (focusState.isFocused) {
// 延迟一点确保 IME 已弹出
LaunchedEffect(Unit) {
bringIntoViewRequester.bringIntoView()
}
}
}
)
}
}

💡 注意:bringIntoView() 在某些版本 Compose 中对 verticalScroll 支持有限,更可靠的方式是直接滚动到底部

1
2
3
LaunchedEffect(Unit) {
scrollState.animateScrollTo(scrollState.maxValue)
}

常见错误排查

问题 解决方案
设置了 imePadding() 但没效果 检查 AndroidManifest.xml 是否有 adjustResize
使用 BoxConstraintLayout 包裹 改用 Column/LazyColumn + verticalScroll
全屏模式(沉浸式)导致 IME 不触发 resize 避免使用 WindowInsetsControllerCompat 隐藏状态栏/导航栏,或手动处理 insets
使用 DialogBottomSheet 它们默认不响应 adjustResize,需手动监听 WindowInsets.ime

手动监听 IME 高度(备用方案)

如果 imePadding() 不满足需求,可手动获取 IME 高度:

1
2
3
4
5
6
7
8
9
val imeHeight = WindowInsets.ime.getBottom(LocalDensity.current)

Box {
// 内容
Column(Modifier.fillMaxSize()) { /* ... */ }

// 底部安全区
Spacer(Modifier.height(imeHeight.toDp()))
}

但通常 imePadding() 已足够。

总结

要让 Compose 输入框不被键盘遮挡,请按顺序检查:

  1. AndroidManifest.xml 中 Activity 设置 android:windowSoftInputMode="adjustResize"
  2. ✅ 布局使用可滚动容器(LazyColumnverticalScroll
  3. ✅ 在容器上加 .imePadding()