6、Jetpack Compose 入门 --- 文字

553 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 6 天,点击查看活动详情

一、文字的显示

参数总览

Jetpack Compose 中的Text,类似于Android中的TextView,其作用是用来显示一段文字内容。它有两个构造函数:

fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
)

fun Text(
    text: AnnotatedString,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    inlineContent: Map<String, InlineTextContent> = mapOf(),
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
)

这两个构造函数的区别在text参数,一个是纯粹的String类型,另一个是AnnotatedString类型,通过源码可以得知AnnotatedString的本质是CharSequence,另外,第二个构造函数中多了一个inlineContent参数。
下面来看看部分参数的解释:

参数类型描述可选值
textString要显示的文本
textAnnotatedString要显示的文本
modifierModifier修饰符
colorColor文本颜色
fontSizeTextUnit在绘制文本时使用的字形大小单位:sp
fontStyleFontStyle在绘制字母时使用的字体变体FontStyle(val value: Int)
FontStyle.Normal:默认字形
FontStyle.Italic:斜体
fontWeightFontWeight在绘制文本时使用的字体厚度FontWeight(val weight: Int)
Thin:100
ExtraLight:200
Light:300
Normal:400
Medium:500
SemiBold:600
Bold:700
ExtraBold:800
Black:900
fontFamilyFontFamily在渲染文本时要使用的字体系列Default:默认字体
SansSerif:无衬线字体
Serif:有衬线字体
Monospace:等宽字体
Cursive:草书
letterSpacingTextUnit每个字母之间添加的空间量单位:sp
textDecorationTextDecoration在文本上绘制的装饰None:不绘制横线
Underline:下划线
LineThrough:删除线
textAlignTextAlign段落行中文本的对齐方式Left:左对齐
Right:右对齐
Center:居中对齐(注意只会横向居中
Justify:感觉是两端对齐。官方解释是拉伸线条以软线断裂结束以填充容器的宽度,以硬线断裂结束的线路朝向启动边缘对齐。但我不知道为什么就是不能实现这个效果
Start:按文字方向的起始位置对齐
End:按文字方向的结束位置对齐
lineHeightTextUnit段落的线路高度单位:sp
overflowTextOverflow文字溢出时的视觉方案Clip:当绘制区域不够时,直接剪切掉超出的部分
Ellipsis:当绘制区域不够时,用省略号表示
Visible:当绘制区域不够时,允许靠近边界的文字以超出边界的方式完成绘制
softWrapBoolean文本是否应在软线断裂中断Boolean
maxLinesInt文本的可选最大行数
onTextLayout(TextLayoutResult) -> Unit在计算新文本布局时执行的回调

参数图解

fontStyle:在绘制字母时使用的字体变体

可选值为 Normal、Italic,其他值均为无效值。

image.png


fontWeight:在绘制文本时使用的字体厚度

默认提供了100~900的9个厚度值,效果分别如下:

image.png image.png image.png


fontFamily:在渲染文本时要使用的字体系列

image.png image.png


letterSpacing:每个字母之间添加的空间量

image.png


textDecoration:在文本上绘制横线

image.png


textAlign:段落行中文本的对齐方式

image.png image.png image.png image.png image.png image.png


lineHeight:段落的线路高度(行高)

image.png image.png


overflow:文字溢出时的视觉方案

image.png image.png image.png


softWrap:文本是否应在软线断裂中断

image.png

举例

显示普通的文字

除了最普通的Text(text = "Hello World")之外,还可以显示资源中的文字Text(text = stringResource(R.string.hello_world))

显示一段包含多种样式的文字

先看一个列子:

image.png

Text(text = buildAnnotatedString {
    append("通过AnnotatedString,可以实现给文字指定")
    withStyle(style = SpanStyle(color = Color.Red)) {
        append(text = "各")
    }
    withStyle(style = SpanStyle(color = Color.Green)) {
        append(text = "种")
    }
    withStyle(style = SpanStyle(color = Color.Cyan)) {
        append(text = "颜")
    }
    withStyle(style = SpanStyle(color = Color.Blue)) {
        append(text = "色")
    }
    append("、")
    withStyle(style = SpanStyle(background = Color.Yellow)) {
        append(text = "背景色")
    }
    append("、")
    withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
        append(text = "下划线")
    }
    append("、")
    withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
        append(text = "删除线")
    }
    append("、")
    withStyle(style = SpanStyle(fontSize = 18.sp)) {
        append(text = "变大")
    }
    append("、")
    withStyle(style = SpanStyle(fontSize = 10.sp)) {
        append(text = "变小")
    }
    append("、")
    withStyle(style = SpanStyle(fontWeight = FontWeight.Black)) {
        append(text = "黑体")
    }
    append("、")
    withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
        append(text = "粗体")
    }
    append("、")
    withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
        append(text = "斜体")
    }
    append("、")
    withStyle(style = SpanStyle(baselineShift = BaselineShift.Superscript)) {
        append(text = "上标")
    }
    append("、")
    withStyle(style = SpanStyle(baselineShift = BaselineShift.Subscript)) {
        append(text = "下标")
    }
    append("、")
    withStyle(style = SpanStyle(textGeometricTransform = TextGeometricTransform(scaleX = 0.5f))) {
        append(text = "缩")
    }
    withStyle(style = SpanStyle(textGeometricTransform = TextGeometricTransform(scaleX = 2f))) {
        append(text = "放")
    }
    append("、")
    withStyle(style = SpanStyle(shadow = Shadow(color = Color.Gray, offset = Offset(x = 10f, y = 10f), blurRadius = 0.5f))) {
        append(text = "阴影")
    }
    append("等")
})

在给一段文字中的每部分设置不同的样式时,需要用到AnnotatedString类。

@Immutable
class AnnotatedString internal constructor(
    val text: String,
    val spanStyles: List<Range<SpanStyle>> = emptyList(),
    val paragraphStyles: List<Range<ParagraphStyle>> = emptyList(),
    internal val annotations: List<Range<out Any>> = emptyList()
) : CharSequence

从源码可以看到,AnnotatedString它继承于CharSequence,是一个数据类,其中包含:

  • text:一个Text值。
  • spanStyles:用于指定位置范围的文字样式,可应用于一组指定的字符。
  • paragraphStyles:用于指定文字对齐、文字方向、行高和文字缩进样式,可应用于整个段落。

二、用户互动

选择文字

GIF 2022-3-20 23-33-54.gif

要启用文字选择功能,需要使用SelectionContainer可组合项封装文字元素:

SelectionContainer {
    Text(text = "这是一段长按后可选择的文字")
}

设置部分文字可点击

GIF 2022-3-23 22-58-53.gif

我们可以通过pushStringAnnotation来实现此功能:

  1. 调用pushStringAnnotation,设置一个标记tag和一个注解annotation
  2. 指定一段文字,并通过调用pop方法来标明前一个注解的结束位置。
  3. 通过AnnotatedStringgetStringAnnotations方法,根据tag取出annotaion
  4. 根据annotion的内容执行对应的操作。
val tag = "注解的tag"
val tagText = buildAnnotatedString {
    append(text = "还可以给指定的文字")
    pushStringAnnotation(tag = tag, annotation = "点击事件Action")
    withStyle(style = SpanStyle(color = Color.Blue)) {
        append(text = "设置点击事件")
    }
    pop()
}
// 构造可点击文本
ClickableText(text = tagText, onClick = { index ->
    // 根据tag取出annotation并打印
    tagText.getStringAnnotations(tag = tag, start = index, end = index).firstOrNull()?.let { annotation ->
        Toast.makeText(context, "点击了:【${annotation.item}】", Toast.LENGTH_SHORT).show()
    }
})

三、文字的输入

Jetpack Compose 中主要的文字输入组件有两个,TextFieldBasicTextField,类似于Android中的EditText

1、TextField

先看一个最简单的使用

GIF 2022-3-24 23-36-47.gif

val valueState = remember { mutableStateOf("这是初始化内容") }
TextField(value = valueState.value,
    onValueChange = { valueState.value = it })

因为Compose遵循的是单向数据流的原则,因此要做到内容的输入,需要使用到State,仅仅是value = "这是初始化内容"这样是没法达到输入效果的。

TextField 部分参数

参数类型描述可选值
valueString显示在输入框中的内容
onValueChange(String) -> Unit更新文本时触发的回调,回调的参数就是更新的文本
modifierModifier修饰符
enabledBoolean启用状态如果为false,文本字段既不是可编辑的,也不是可聚焦的,输入的内容将不可选择,并且视觉文本将出现在禁用的UI状态中
readOnlyBoolean可编辑状态如果为true,则无法修改文本内容,但是,用户可以获取焦点并复制文本,通常用于显示用户无法编辑的预先填充的表单
textStyleTextStyle文本样式默认值为 LocalTextStyle.current
label@Composable(() -> Unit)?标签内容
placeholder@Composable (() -> Unit)?处于焦点并输入文本为空要显示的可选占位符
leadingIcon@Composable (() -> Unit)?前导图标显示在组件的开头
trailingIcon@Composable (() -> Unit)?尾随图标显示在组件的末尾
isErrorBoolean是否错误的标识表示文本字段的当前值是否错误
visualTransformationVisualTransformation视觉转换如:使用PasswordVisualTransformation创建密码文本
keyboardOptionsKeyboardOptions软键盘配置capitalization:通知键盘是否自动自动大写字符,单词或句子。
autoCorrect:通知键盘是否启用自动正确。
keyboardType:键盘类型。如文本、电话号码、邮件、密码等。
imeAction:IME动作。如下一步、前往、发送等
keyboardActionsKeyboardActions软键盘行为配置软键盘不同IME动作的行为。
singleLineBoolean是否单行
maxLinesInt最大行数
interactionSourceMutableInteractionSource交互的状态信息
shapeShape形状组件的外观
colorsTextFieldColors颜色集配置组件各状态下各部分的文本颜色

举例

  • 设置了label、placeholder
  • 支持切换可用状态,支持切换只读状态,支持切换为输入密码样式
  • 当内容中包含"ERROR"时,输入的内容不合格
  • IME按键功能为输出输入框中的内容

GIF 2022-4-5 18-33-39.gif

Column {
    val valueState = remember { mutableStateOf("输入内容") }
    val enabledState = remember { mutableStateOf(true) }
    val readOnlyState = remember { mutableStateOf(false) }
    val textStyleState = remember { mutableStateOf(TextStyle.Default) }
    val isErrorState = valueState.value.contains("ERROR")
    val visualTransformationState = remember { mutableStateOf(VisualTransformation.None) }
    val outputValueState = remember { mutableStateOf("") }
    TextField(
        value = valueState.value,
        onValueChange = { valueState.value = it },
        enabled = enabledState.value,
        readOnly = readOnlyState.value,
        textStyle = textStyleState.value,
        label = { Text(text = "label:包含"ERROR"为不合格内容") },
        placeholder = { Text(text = "placeholder") },
        leadingIcon = { Icon(painter = painterResource(id = R.drawable.ic_android_black_24dp), contentDescription = null) },
        trailingIcon = {
            Icon(
                painter = painterResource(id = R.drawable.ic_baseline_clear_24),
                contentDescription = null,
                modifier = Modifier.clickable { valueState.value = "" })
        },
        isError = isErrorState,
        visualTransformation = visualTransformationState.value,
        keyboardOptions = KeyboardOptions(
            capitalization = KeyboardCapitalization.Characters,
            autoCorrect = true,
            keyboardType = KeyboardType.Text,
            imeAction = ImeAction.Go
        ),
        keyboardActions = KeyboardActions(onGo = {
            outputValueState.value = valueState.value
        }),
        singleLine = true,
        maxLines = 1,
        shape = RoundedCornerShape(8.dp),
    )
    Spacer(modifier = Modifier.height(4.dp))
    Row {
        Button(onClick = { enabledState.value = !enabledState.value }) {
            Text(text = "切换可用状态:${if (enabledState.value) "可用" else "不可用"}")
        }
        Spacer(modifier = Modifier.width(4.dp))
        Button(onClick = { readOnlyState.value = !readOnlyState.value }) {
            Text(text = "切换只读状态:${if (readOnlyState.value) "只读" else "可读可写"}")
        }
        Spacer(modifier = Modifier.width(4.dp))
        Button(onClick = {
            visualTransformationState.value = if (visualTransformationState.value is PasswordVisualTransformation) {
                VisualTransformation.None
            } else {
                PasswordVisualTransformation()
            }
        }) {
            Text(text = "切换为${if (visualTransformationState.value is PasswordVisualTransformation) "正常" else "密码"}样式")
        }
    }
    Spacer(modifier = Modifier.height(4.dp))
    Text(buildAnnotatedString {
        append("输入内容:")
        if (isErrorState) {
            withStyle(style = SpanStyle(color = Color.Red)) {
                append("不合格")
            }
        } else {
            withStyle(style = SpanStyle(color = Color.Green)) {
                append("合格")
            }
        }
    })
    Spacer(modifier = Modifier.height(4.dp))
    Text(text = "点击软键盘IME按钮输出输入框内容:${outputValueState.value}")
}

2、BasicTextField

BasicTextFieldTextField相比的区别就在于:

TextFieldBasicTextField
是否遵循 Material Design\color{#00FF00}{√}×\color{red}{×}
默认样式填充样式轮廓样式
是否提供了占位符等装饰\color{#00FF00}{√}×\color{red}{×}

BasicTextField 部分参数

参数类型描述说明
cursorBrushBrush光标颜色默认为SolidColor(Color.Black)
decorationBox@Composable (innerTextField: @Composable () -> Unit) -> Unit可组合Lambda允许在文本字段周围添加装饰,例如图标,占位符,辅助消息或类似,并自动增加文本字段的命中目标区域。允许您控制内部文本字段相对于装饰的位置,文本的实现将通过你提供的innerTextFieldLambda装饰盒框架来控制。