文本是所有UI系统中非常重要的一个种元素,文本的输入在UI框架中的重要性也特别的高,因为这是最重要的一种用户输入。今天专注于文本的输入处理,包括文本输入框,以及文本的选择和富式点击处理。
文本输入
Compose提供了符合Material Design 的文本输入TextField ),默认的实现是全填充的:
1
2
3
4
5
6
7
8
9
10
@Composable
fun SimpleFilledTextFieldSample () {
var text by remember { mutableStateOf ( "Hello" ) }
TextField (
value = text ,
onValueChange = { text = it },
label = { Text ( "Label" ) }
)
}
还有一个边框式的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" ) }
)
}
可以看到TextField函数最关键的有三个参数:文本框中的显示的文本text,文本变化回调onValueChange,提示标签label。需要注意传给text的变量要是状态(State),这样才会触发重组,否则TextField显示的文本不会发生变化。
定制TextField
可以通过其他的参数来控制输入框的行为,最为常用的就是行数限制singleLine和maxLines, 以及文本的样式控制textStyle,它可以控制文本颜色和字体:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun StyledTextField () {
var value by remember { mutableStateOf ( "Hello\nWorld\nInvisible" ) }
TextField (
value = value ,
onValueChange = { value = it },
label = { Text ( "Enter text" ) },
maxLines = 2 ,
textStyle = TextStyle ( color = Color . Blue , fontWeight = FontWeight . Bold ),
modifier = Modifier . padding ( 20. dp )
)
}
textStyle比较丰富,除了直接指定颜色以外,还可以用Brush API ,以实现一些颜色渐变,渐变效果是针对整个输入框的,换言之不同的行效果是一样的:
1
2
3
4
5
6
7
8
9
var text by remember { mutableStateOf ( "" ) }
val brush = remember {
Brush . linearGradient (
colors = listOf ( Color . RED , Color . YELLOW , Color . GREEN , Color . BLUE )
)
}
TextField (
value = text , onValueChange = { text = it }, textStyle = TextStyle ( brush = brush )
)
与键盘联动
TextField能够配置软件盘以实现特定输入样式,比如只输入数字,只有英文字符等等,通过TextField的keyboardOptions参数,传入一个KeyboardOptions )对象。常用的配置项有:
当输入完成后,用户点了imeAction指定的按扭后,可以指定回调函数以执行相关的操作,通过keyboardActions参数指定一个KeyboardActions 对象,里面可以指定对应于imeAction中的各种回调,如onSearch会在imeAction指定为Search时,用户点击后触发;onSend会在imeAction是Send时,用户点击触发,等等。
特殊形式的输入
有些特殊的场景是不能够直接把用户的输入文本直接的展现在框里,比如输入密码时,再比如像输入电话号码时,可能会自动在3个数字后面加上短横线。这时就需要用到VisualTransformation 来对文本进行转换处理:
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 )
)
}
文本状态管理
TextField的文本(text参数)是需要转换成状态的,这样才能更好的触发重组。基础的通用的TextField使用方式是,把文本转成状态,塞给TextField,然后在其onValueChange中再更新此状态:
1
2
3
4
5
6
7
8
// ...
var text by remember { mutableStateOf ( "" ) }
TextField (
value = text ,
onValueChange = { text = it },
)
// ...
但现实的代码不可能这么简单,用户的输入必然会有业务逻辑去处理,所以onValueChange肯定会调用ViewModel去处理用户输入。那么自然也要从ViewModel处获得。但由于TextField的特殊性,仍然要把使用MutableState来定义状态,而不能用响应式的Reactive stream或者StateFlow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SignUpViewModel ( private val userRepository : UserRepository ) : ViewModel () {
var username by mutableStateOf ( "" )
private set
fun updateUsername ( input : String ) {
username = input
}
}
// SignUpScreen.kt
@Composable
fun SignUpScreen ( /*...*/ ) {
OutlinedTextField (
value = viewModel . username ,
onValueChange = { username -> viewModel . updateUsername ( username ) }
/*...*/
)
}
文本的选择
除了文本输入以外,文本显示的选择也视为文字编辑的一种方式,因为选择之后就可以执行复制或者搜索等全局操作。Compose提供了细粒度的可交互式文本显示控制。Text本身是不支持选择的(Not Selectable),自然也就无法复制。可以使用SelectableContainer )来包裹Text以实现可选择(Selectable):
1
2
3
4
5
6
@Composable
fun SelectableText () {
SelectionContainer {
Text ( "This text is selectable" )
}
}
并且,可选择区域可以跨多个Text。与之相对的,还有不可选择函数DisableSelection ),比如一大片可选择文本中,想让某一小块文本不能被选择,这时DisableSelection就派上用场了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun PartiallySelectableText () {
SelectionContainer {
Column {
Text ( "This text is selectable" )
Text ( "This one too" )
Text ( "This one as well" )
DisableSelection {
Text ( "But not this one" )
Text ( "Neither this one" )
}
Text ( "But again, you can select this one" )
Text ( "And this one too" )
}
}
}
可以看出对于文本的选择控制还是相当的灵活的(flexible)。
富式文本点击
对于针对 整个文本的点击事件可以用Modifier中的clickable .clickable(kotlin.Boolean,kotlin.String,androidx.compose.ui.semantics.Role,kotlin.Function0))函数来处理,这跟常规的Composable没区别都一样的。但对于文本来说有更为细腻的点击事件处理,包括获取具体点击的光标位置,以及富式文本点击,也即针对 文本中不同部分的响应。
获取点击的光标位置
想要获取到文本中点击的光标位置,其实也就是点击的是第几个字符,可以用ClickableText ),它有一个自己的onClick回调函数,里面的参数是一个offset表示被点击字符的索引:
1
2
3
4
5
6
@Composable
fun SimpleClickableText () {
ClickableText ( text = AnnotatedString ( "Click Me" ), onClick = { offset ->
Log . d ( "ClickableText" , "$offset -th character is clicked." )
})
}
注意onClick的参数是文本字符串的索引,从0开始。这个索引一般用来确定点击的富文本中的某一个标记(Annotation)。
富文本的点击处理
Text是支持富文本的(基于AnnotatedString )。通过ClickableText中onClick的索引参数,就能知道点击的具体是哪个Annotation。比如一个超链接标记,具体的URL对用户是不可见的,作为额外的Tag信息在Annotation中,通过索引判断当点击到了超链接上面时,可以跳转到此URL:
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 AnnotatedClickableText () {
val annotatedText = buildAnnotatedString {
append ( "Click " )
// We attach this *URL* annotation to the following content
// until `pop()` is called
pushStringAnnotation (
tag = "URL" , annotation = "https://developer.android.com"
)
withStyle (
style = SpanStyle (
color = Color . Blue , fontWeight = FontWeight . Bold
)
) {
append ( "here" )
}
pop ()
}
ClickableText ( text = annotatedText , onClick = { offset ->
// We check if there is an *URL* annotation attached to the text
// at the clicked position
annotatedText . getStringAnnotations (
tag = "URL" , start = offset , end = offset
). firstOrNull () ?. let { annotation ->
// If yes, we log its value
Log . d ( "Clicked URL" , annotation . item )
}
})
}
总结
本文介绍了两种最常规的文本编辑,一是文本输入,一个是文本的选择和点击,这些都是日常项目开发中的非常常见的需求。Jetpack Compose对文本的操作提供了非常友好的支持,能够应付绝大部分的需求场景。
References