【Compose】输入框(TextField)点击空白处失焦并关闭软键盘
在移动应用开发中,我们经常需要实现点击输入框外部(空白处)时,输入框失去焦点并关闭软键盘的功能。这是一个常见的用户体验需求,能够提升应用的交互体验。
实现方案对比
传统方案:使用LocalFocusManager
kotlin
@Composable
fun ClickOutsideToDismiss() {
val focusManager = LocalFocusManager.current
Box(
modifier = Modifier
.fillMaxSize()
.clickable { focusManager.clearFocus() }
) {
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.fillMaxWidth()
)
}
}
问题:
- 在最新版本的Compose BOM中,
LocalFocusManager.current.clearFocus()对 Android 8 以下版本不兼容 - 在低版本Android设备上可能无法正常工作
新方案:全局焦点请求器方案
为了解决传统方案的兼容性问题,我们采用全局焦点请求器的方案,通过创建一个可以获取焦点的不可见Box来管理焦点状态。
核心实现逻辑
1. 全局焦点请求器
kotlin
object GlobalFocusRequester {
val dumpRequester = FocusRequester()
}
创建一个全局的焦点请求器对象,用于在整个应用中统一管理焦点。
2. 自定义主题包装器
kotlin
@Composable
fun CustomTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = {
Box {
// 全局焦点容器
Box(
modifier = Modifier
.size(0.dp)
.focusRequester(GlobalFocusRequester.dumpRequester)
.focusable()
)
content()
}
}
)
}
在应用的根组件中添加一个全局的、可以获取焦点的不可见Box,这个Box将作为焦点的"接收器"。
3. 清除焦点修饰符
kotlin
@Composable
fun Modifier.clearFocus(): Modifier {
return pointerInput(this) {
detectTapGestures(
onPress = {
GlobalFocusRequester.dumpRequester.requestFocus()
}
)
}
}
创建一个修饰符,当点击时请求全局焦点,从而让其他输入框失去焦点。
完整实现代码
1. 全局焦点管理器
kotlin
object GlobalFocusRequester {
val dumpRequester = FocusRequester()
}
2. 清除焦点修饰符
kotlin
@Composable
fun Modifier.clearFocus(): Modifier {
return pointerInput(this) {
detectTapGestures(
onPress = {
GlobalFocusRequester.dumpRequester.requestFocus()
}
)
}
}
3. 自定义主题
kotlin
@Composable
fun CustomTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = if (darkTheme) darkColorScheme else lightColorScheme,
typography = Typography,
content = {
Box {
// 全局焦点容器
Box(
modifier = Modifier
.size(0.dp)
.focusRequester(GlobalFocusRequester.dumpRequester)
.focusable()
)
content()
}
}
)
}
4. 支持失焦的自定义弹窗
在Compose中,Dialog和ModalBottomSheet都是独立的window,它们有自己的焦点管理机制。在自定义弹窗中直接使用GlobalFocusRequester.dumpRequester.requestFocus()是无法生效的,因为弹窗的焦点管理是独立的。
kotlin
@Composable
fun CustomDialog(
show: Boolean,
close: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
if (show) {
Dialog(
onDismissRequest = close,
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable { close() }
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clearFocus(),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
content()
}
}
}
}
}
}
@Composable
fun BottomDialog(
show: Boolean,
close: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
if (show) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = close,
sheetState = sheetState,
sheetGesturesEnabled = false,
dragHandle = {},
containerColor = Color.Transparent,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = false,
shouldDismissOnClickOutside = false
)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clearFocus()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
content()
}
}
}
}
}
重要说明:
- 在自定义弹窗中,由于弹窗是独立的window,必须为每个弹窗单独定义焦点管理逻辑
- 不能直接使用
GlobalFocusRequester.dumpRequester.requestFocus(),需要在弹窗内部创建独立的焦点请求器 - 对于Dialog,可以使用
DialogProperties来控制焦点和点击行为 - 对于ModalBottomSheet,需要在弹窗内部添加清除焦点的修饰符
使用示例
1. 应用入口设置
kotlin
@Composable
fun MyApp() {
CustomTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen()
}
}
}
2. 主界面示例
kotlin
@Composable
fun MainScreen() {
var text by remember { mutableStateOf("") }
var showDialog by remember { mutableStateOf(false) }
var dialogText by remember { mutableStateOf("") }
var showBottomDialog by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.clearFocus() // 点击空白处失焦
) {
TextField(
value = text,
onValueChange = { text = it },
label = { Text("输入框1") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
value = text,
onValueChange = { text = it },
label = { Text("输入框2") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { showDialog = true }) {
Text("打开弹窗")
}
CustomDialog(
show = showDialog,
close = { showDialog = false }
) {
TextField(
value = dialogText,
onValueChange = { dialogText = it },
label = { Text("对话框输入框") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
dialogText = ""
// 处理保存逻辑
}
) {
Text("保存")
}
}
BottomDialog(
show = showBottomDialog,
close = { showBottomDialog = false }
) {
TextField(
value = dialogText,
onValueChange = { dialogText = it },
label = { Text("底部弹窗输入框") },
modifier = Modifier.fillMaxWidth()
)
}
}
}
3. 弹窗使用示例
kotlin
// 在Activity或ViewModel中管理弹窗状态
var showCustomDialog by remember { mutableStateOf(false) }
var showBottomSheet by remember { mutableStateOf(false) }
// 使用自定义Dialog
CustomDialog(
show = showCustomDialog,
close = { showCustomDialog = false }
) {
Column {
TextField(
value = "输入内容",
onValueChange = { /* 更新状态 */ },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { showCustomDialog = false }
) {
Text("关闭")
}
}
}
// 使用BottomDialog
BottomDialog(
show = showBottomSheet,
close = { showBottomSheet = false }
) {
TextField(
value = "底部弹窗输入",
onValueChange = { /* 更新状态 */ },
modifier = Modifier.fillMaxWidth()
)
}
方案优势
优点:
- 兼容性好:适用于所有Android版本,包括Android 8以下
- 实现简单:只需要添加全局焦点容器和清除焦点修饰符
- 使用方便 :只需要在需要的地方添加
.clearFocus()修饰符 - 性能优秀:无需额外的计算或状态管理
- 可扩展性强:可以轻松集成到现有项目中
缺点:
- 需要全局配置:需要在应用根组件中设置CustomTheme
- 焦点管理集中化:所有焦点都通过全局焦点请求器管理,可能在复杂场景下需要更精细的控制
注意事项
- 确保CustomTheme作为根组件:全局焦点容器必须在应用的根组件中设置
- 焦点请求器生命周期:确保GlobalFocusRequester.dumpRequester在组件生命周期内可用
- 多窗口场景:在多窗口场景下,可能需要额外的焦点管理逻辑
- 测试覆盖:建议在不同Android版本上测试焦点管理功能
总结
本文介绍了一种通过全局焦点请求器来实现输入框点击空白处失焦并关闭软键盘的方案。这个方案解决了传统LocalFocusManager在低版本Android上的兼容性问题,提供了简单、可靠的解决方案。
通过在应用根组件中添加一个全局的、可以获取焦点的不可见Box,配合清除焦点修饰符,我们可以在整个应用中实现统一的焦点管理。这种方法不仅实现简单,而且具有良好的兼容性和扩展性,适合大多数Android应用场景。