降Compose十八掌之『双龙取水』| Text Edit

公众号「稀有猿诉」

文本是所有UI系统中非常重要的一个种元素,文本的输入在UI框架中的重要性也特别的高,因为这是最重要的一种用户输入。今天专注于文本的输入处理,包括文本输入框,以及文本的选择和富式点击处理。

文本输入

Compose提供了符合Material Design的文本输入TextField,默认的实现是全填充的:

Kotlin 复制代码
@Composable
fun SimpleFilledTextFieldSample() {
    var text by remember { mutableStateOf("Hello") }

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

还有一个边框式的OutlinedTextField

Kotlin 复制代码
@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,它可以控制文本颜色和字体:

Kotlin 复制代码
@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,以实现一些颜色渐变,渐变效果是针对整个输入框的,换言之不同的行效果是一样的:

Kotlin 复制代码
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来对文本进行转换处理:

Kotlin 复制代码
@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中再更新此状态:

Kotlin 复制代码
    // ...
    var text by remember { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = { text = it },
    )
    // ...

但现实的代码不可能这么简单,用户的输入必然会有业务逻辑去处理,所以onValueChange肯定会调用ViewModel去处理用户输入。那么自然也要从ViewModel处获得。但由于TextField的特殊性,仍然要把使用MutableState来定义状态,而不能用响应式的Reactive stream或者StateFlow:

Kotlin 复制代码
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):

Kotlin 复制代码
@Composable
fun SelectableText() {
    SelectionContainer {
        Text("This text is selectable")
    }
}

并且,可选择区域可以跨多个Text。与之相对的,还有不可选择函数DisableSelection,比如一大片可选择文本中,想让某一小块文本不能被选择,这时DisableSelection就派上用场了:

Kotlin 复制代码
@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函数来处理,这跟常规的Composable没区别都一样的。但对于文本来说有更为细腻的点击事件处理,包括获取具体点击的光标位置,以及富式文本点击,也即针对 文本中不同部分的响应。

获取点击的光标位置

想要获取到文本中点击的光标位置,其实也就是点击的是第几个字符,可以用ClickableText,它有一个自己的onClick回调函数,里面的参数是一个offset表示被点击字符的索引:

Kotlin 复制代码
@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:

Kotlin 复制代码
@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

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
众拾达人2 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌3 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley4 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei6 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng6 小时前
安卓多渠道apk配置不同签名
android
枫_feng7 小时前
AOSP开发环境配置
android·安卓
叶羽西7 小时前
Android Studio打开一个外部的Android app程序
android·ide·android studio
qq_171538859 小时前
利用Spring Cloud Gateway Predicate优化微服务路由策略
android·javascript·微服务
Vincent(朱志强)10 小时前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
mmsx10 小时前
android 登录界面编写
android·登录界面