Jetpack Compose中的基本组件-文本输入框、键盘收起

前言

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

  • BasicTextField

  • TextField

  • OutlinedTextField

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

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

BasicTextField

基础示例

1
2
3
4
BasicTextField(
value = "",
onValueChange = {},
)

文本渐变和光标渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable  // @Composable 注解表示该函数可以作为可组合项使用
fun GradientTextField() { // GradientTextField 函数用于创建一个带有渐变效果的文本输入框
var text by remember { mutableStateOf("") } // mutableStateOf 创建一个可变状态
BasicTextField( // BasicTextField 是一个基本的文本输入框
value = text,
onValueChange = { text = it },
textStyle = TextStyle( // textStyle 设置文本的样式
brush = Brush.linearGradient( // linearGradient 设置线性渐变
colors = listOf(Color.Red, Color.Blue, Color.Green, Color.Magenta)
),
fontSize = 32.sp
),
cursorBrush = Brush.verticalGradient( // verticalGradient 设置垂直渐变
colors = listOf(Color.Blue, Color.Cyan, Color.Red, Color.Magenta)
),
)
}

placeholder

默认是不支持placeholder的,我们可以自定义实现。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun BasicTextFieldWithPlaceholder(
fontSize: Int,
placeholder: String,
onFocusChange: (focus: Boolean) -> Unit,
onValueChange: (String) -> Unit
) {
var text by remember { mutableStateOf("") }

BasicTextField(
value = text,
modifier = Modifier
.fillMaxSize()
.focusable()
.onFocusChanged { focusState ->
onFocusChange(focusState.isFocused)
},
onValueChange = { text = it;onValueChange(it) },
singleLine = true,
textStyle = TextStyle(
color = Color(0xffeeeeee),
fontSize = fontSize.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = 8.dp, end = 8.dp)
) {
if (text.isBlank()) {
Text(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center),
text = placeholder,
style = TextStyle(color = Color.White),
fontSize = fontSize.sp
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
) {
innerTextField()
}
}
},
cursorBrush = SolidColor(Color.White),
)
}

TextField

1
2
3
4
5
6
7
8
9
@Composable
fun SimpleFilledTextFieldSample() {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("姓名") }
)
}

输入类型为密码

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun PasswordTextField() {
var password by rememberSaveable { mutableStateOf("") }

TextField(
value = password,
onValueChange = { password = it },
label = { Text("Enter password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
}

彩虹色文字

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun SimpleFilledTextFieldSample() {
var text by remember { mutableStateOf("") }
val brush = remember {
Brush.linearGradient(
colors = listOf<Color>(Color.Red, Color.Green, Color.Blue)
)
}
TextField(
value = text, onValueChange = { text = it }, textStyle = TextStyle(brush = brush)
)
}

OutlinedTextField

1
2
3
4
5
6
7
8
9
10
@Composable
fun SimpleOutlinedTextFieldSample() {
var text by remember { mutableStateOf("") }

OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("Label") }
)
}

收起键盘

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
@Composable
fun BasicTextFieldWithDismissKeyboard() {
// 获取焦点管理器(用于清除焦点)
val focusManager = LocalFocusManager.current
// 可选:获取键盘控制器(用于强制收起键盘)
val keyboardController = LocalSoftwareKeyboardController.current

// 父布局:点击时清除焦点(收起键盘)
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
// 点击空白区域时清除焦点
.clickable {
focusManager.clearFocus() // 清除所有焦点,键盘会自动收起
// 可选:强制收起键盘(某些场景下需要)
// keyboardController?.hide()
}
// 防止点击事件被子组件消费后父组件无法响应
.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 等输入组件)。

去除涟漪效果

只要组件设置的可点击,就会出现涟漪效果,要想去除涟漪效果,我们可以扩展Modifier的方法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import kotlinx.coroutines.delay

inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
clickable(indication = null,
interactionSource = remember { MutableInteractionSource() }) {
onClick()
}
}

使用方法:

clickable更换为noRippleClickable即可。

两种组件封装形式

在对 BasicTextField 进行组件封装时,既可以使用 MutableState 也可以使用回调来在外层获取组件内的值。

下面分别介绍这两种方式的特点和使用场景。

使用 MutableState

原理

通过将 MutableState 对象作为参数传递给封装的 BasicTextField 组件,组件内部直接操作这个 MutableState 的值,由于 MutableState 是引用类型,所以在组件外部也能感知到其值的变化。

示例代码

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
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

@Composable
fun CustomBasicTextField(textState: MutableState<String>) {
var text by textState
BasicTextField(
value = text,
onValueChange = { newText ->
text = newText
}
)
}

// 使用示例
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication {
val textState = mutableStateOf("")
CustomBasicTextField(textState)
// 可以直接访问 textState 的值
val currentText = textState.value
println("当前文本值: $currentText")
}

优缺点

优点

  • 代码简洁:使用 MutableState 可以减少回调函数的使用,使代码逻辑更加清晰。
  • 数据流向明确:直接操作状态对象,数据的流动方向一目了然,便于理解和维护。

缺点

  • 耦合度较高:组件与外部的状态紧密绑定,降低了组件的独立性和可复用性。

使用回调

原理

定义一个回调函数作为参数传递给封装的 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
28
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

@Composable
fun CustomBasicTextField(onTextChanged: (String) -> Unit) {
var text by mutableStateOf("")
BasicTextField(
value = text,
onValueChange = { newText ->
text = newText
onTextChanged(newText)
}
)
}

// 使用示例
import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication {
var currentText by mutableStateOf("")
CustomBasicTextField { newText ->
currentText = newText
println("当前文本值: $currentText")
}
}

优缺点

优点

  • 解耦性强:组件与外部的耦合度较低,组件只负责触发回调,不关心外部如何处理数据,提高了组件的可复用性。
  • 灵活性高:可以根据需要在回调函数中添加更多的逻辑,比如数据验证、格式化等。

缺点

  • 代码复杂度增加:需要定义和管理回调函数,增加了代码的复杂度。

选择建议

  • 使用 MutableState 的场景:当组件与外部的交互比较简单,且数据流向比较明确时,使用 MutableState 可以使代码更加简洁。

    例如,组件只是简单地修改一个状态值,而不需要进行额外的逻辑处理。

  • 使用回调的场景:当组件需要与外部进行复杂的交互,或者需要在值变化时执行一些额外的逻辑时,使用回调可以提高组件的灵活性和可复用性。

    例如,在值变化时需要进行数据验证、发送网络请求等操作。