Jetpack Compose 中的可扩展文本
Expandable Text in Jetpack Compose
所以我正在使用 Text()
可组合项,如下所示:
Text(
text = "this is some sample text that is long and so it is
ellipsized",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
它正确地省略了文本:
问题是我想要在省略号末尾添加一个 See More
标记,提示用户展开可见的文本框。我将如何添加它?
要解决此问题,您需要使用 onTextLayout
获取 TextLayoutResult
:它包含有关绘制文本状态的所有信息。
让它适用于多行是一项棘手的任务。为此,您需要计算椭圆文本和“...查看更多”文本的大小,然后,当您拥有这两个值时,您需要计算需要删除多少文本,以便“...查看更多”非常适合在行尾:
@Composable
fun ExpandableText(
text: String,
modifier: Modifier = Modifier,
minimizedMaxLines: Int = 1,
) {
var cutText by remember(text) { mutableStateOf<String?>(null) }
var expanded by remember { mutableStateOf(false) }
val textLayoutResultState = remember { mutableStateOf<TextLayoutResult?>(null) }
val seeMoreSizeState = remember { mutableStateOf<IntSize?>(null) }
val seeMoreOffsetState = remember { mutableStateOf<Offset?>(null) }
// getting raw values for smart cast
val textLayoutResult = textLayoutResultState.value
val seeMoreSize = seeMoreSizeState.value
val seeMoreOffset = seeMoreOffsetState.value
LaunchedEffect(text, expanded, textLayoutResult, seeMoreSize) {
val lastLineIndex = minimizedMaxLines - 1
if (!expanded && textLayoutResult != null && seeMoreSize != null
&& lastLineIndex + 1 == textLayoutResult.lineCount
&& textLayoutResult.isLineEllipsized(lastLineIndex)
) {
var lastCharIndex = textLayoutResult.getLineEnd(lastLineIndex, visibleEnd = true) + 1
var charRect: Rect
do {
lastCharIndex -= 1
charRect = textLayoutResult.getCursorRect(lastCharIndex)
} while (
charRect.left > textLayoutResult.size.width - seeMoreSize.width
)
seeMoreOffsetState.value = Offset(charRect.left, charRect.bottom - seeMoreSize.height)
cutText = text.substring(startIndex = 0, endIndex = lastCharIndex)
}
}
Box(modifier) {
Text(
text = cutText ?: text,
maxLines = if (expanded) Int.MAX_VALUE else minimizedMaxLines,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textLayoutResultState.value = it },
)
if (!expanded) {
val density = LocalDensity.current
Text(
"... See more",
onTextLayout = { seeMoreSizeState.value = it.size },
modifier = Modifier
.then(
if (seeMoreOffset != null)
Modifier.offset(
x = with(density) { seeMoreOffset.x.toDp() },
y = with(density) { seeMoreOffset.y.toDp() },
)
else
Modifier
)
.clickable {
expanded = true
cutText = null
}
.alpha(if (seeMoreOffset != null) 1f else 0f)
)
}
}
}
@Composable
fun ExpandedText(
text: String,
expandedText: String,
expandedTextButton: String,
shrinkTextButton: String,
modifier: Modifier = Modifier,
softWrap: Boolean = true,
textStyle: TextStyle = LocalTextStyle.current,
expandedTextStyle: TextStyle = LocalTextStyle.current,
expandedTextButtonStyle: TextStyle = LocalTextStyle.current,
shrinkTextButtonStyle: TextStyle = LocalTextStyle.current,
) {
var isExpanded by remember { mutableStateOf(false) }
val textHandler = "${if (isExpanded) expandedText else text} ${if (isExpanded) shrinkTextButton else expandedTextButton}"
val annotatedString = buildAnnotatedString {
withStyle(
if (isExpanded) expandedTextStyle.toSpanStyle() else textStyle.toSpanStyle()
) {
append(if (isExpanded) expandedText else text)
}
append(" ")
withStyle(
if (isExpanded) shrinkTextButtonStyle.toSpanStyle() else expandedTextButtonStyle.toSpanStyle()
) {
append(if (isExpanded) shrinkTextButton else expandedTextButton)
}
addStringAnnotation(
tag = "expand_shrink_text_button",
annotation = if (isExpanded) shrinkTextButton else expandedTextButton,
start = textHandler.indexOf(if (isExpanded) shrinkTextButton else expandedTextButton),
end = textHandler.indexOf(if (isExpanded) shrinkTextButton else expandedTextButton) + if (isExpanded) expandedTextButton.length else shrinkTextButton.length
)
}
ClickableText(
text = annotatedString,
softWrap = softWrap,
modifier = modifier,
onClick = {
annotatedString
.getStringAnnotations(
"expand_shrink_text_button",
it,
it
)
.firstOrNull()?.let { stringAnnotation ->
isExpanded = stringAnnotation.item == expandedTextButton
}
}
)
}
用法
ExpandedText(
text = food.content,
expandedText = food.contentFull,
expandedTextButton = " more",
shrinkTextButton = " less",
textStyle = typographySkModernist().body1.copy(
color = black.copy(alpha = 0.8f)
),
expandedTextStyle = typographySkModernist().body1.copy(
color = black.copy(alpha = 0.8f)
),
expandedTextButtonStyle = typographySkModernist().body1.copy(
color = orange,
),
shrinkTextButtonStyle = typographySkModernist().body1.copy(
color = orange,
),
modifier = Modifier
.padding(top = 32.dp, start = 24.dp, end = 16.dp)
)
我发现发布的解决方案有些矫枉过正。这是一个简单的解决方案:
var showMore by remember { mutableStateOf(false) }
val text =
"Space Exploration Technologies Corp. (doing business as SpaceX) is an American aerospace manufacturer, space transportation services and communications corporation headquartered in Hawthorne, California. SpaceX was founded in 2002 by Elon Musk with the goal of reducing space transportation costs to enable the colonization of Mars. SpaceX manufactures the Falcon 9 and Falcon Heavy launch vehicles, several rocket engines, Cargo Dragon, crew spacecraft and Starlink communications satellites."
Column(modifier = Modifier.padding(20.dp)) {
Column(modifier = Modifier
.animateContentSize(animationSpec = tween(100))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { showMore = !showMore }) {
if (showMore) {
Text(text = text)
} else {
Text(text = text, maxLines = 3, overflow = TextOverflow.Ellipsis)
}
}
}
一个简单的实现:
@Composable
fun ExpandableText(
modifier: Modifier = Modifier,
text: String,
minimizedMaxLines: Int,
style: TextStyle
) {
var expanded by remember { mutableStateOf(false) }
var hasVisualOverflow by remember { mutableStateOf(false) }
Box(modifier = modifier) {
Text(
text = text,
maxLines = if (expanded) Int.MAX_VALUE else minimizedMaxLines,
onTextLayout = { hasVisualOverflow = it.hasVisualOverflow },
style = style
)
if (hasVisualOverflow) {
Row(
modifier = Modifier.align(Alignment.BottomEnd),
verticalAlignment = Alignment.Bottom
) {
val lineHeightDp: Dp = with(LocalDensity.current) { style.lineHeight.toDp() }
Spacer(
modifier = Modifier
.width(48.dp)
.height(lineHeightDp)
.background(
brush = Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.White)
)
)
)
Text(
modifier = Modifier
.background(Color.White)
.padding(start = 4.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { expanded = !expanded }
),
text = "Show More",
color = MaterialTheme.colors.primary,
style = style
)
}
}
}
}
我想要一个更灵活的
/**
* @param modifier use this to add padding and such
* @param longText is the Text that is to long and need to be displayed that has more than [minimizedMaxLines]
* @param minimizedMaxLines (optional) the minimum amount of text lines to be visible in non-expanded state
* @param textAlign (optional) defaults to [TextAlign.Start] unless overridden, try [TextAlign.Justify]
* @param expandHint (optional) this text is appended to the [longText] before expanding and become clickable
* @param shrinkHint (optional) this text is appended to the [longText] after expanding and become clickable
* @param clickColor (optional) denotes the color of the clickable [expandHint] & [shrinkHint] strings
* */
@Composable
fun AppExpandingText(
modifier: Modifier = Modifier,
longText: String,
minimizedMaxLines: Int = 3,
textAlign: TextAlign = TextAlign.Start,
expandHint: String = "… Show More",
shrinkHint: String = "… Show Less",
clickColor: Color = Color.Unspecified
) {
var isExpanded by remember { mutableStateOf(value = false) }
var textLayoutResultState by remember { mutableStateOf<TextLayoutResult?>(value = null) }
var adjustedText by remember { mutableStateOf(value = longText) }
val overflow = textLayoutResultState?.hasVisualOverflow ?: false
val showOverflow = remember { mutableStateOf(value = false) }
val showMore = " $expandHint"
val showLess = " $shrinkHint"
LaunchedEffect(textLayoutResultState) {
if (textLayoutResultState == null) return@LaunchedEffect
if (!isExpanded && overflow) {
showOverflow.value = true
val lastCharIndex = textLayoutResultState!!.getLineEnd(lineIndex = minimizedMaxLines - 1)
adjustedText = longText
.substring(startIndex = 0, endIndex = lastCharIndex)
.dropLast(showMore.length)
.dropLastWhile { it == ' ' || it == '.' }
}
}
val annotatedText = buildAnnotatedString {
if (isExpanded) {
append("$longText ")
withStyle(
style = SpanStyle(
color = MaterialTheme.colors.onSurface,
fontSize = 14.sp,
)
) {
pushStringAnnotation(tag = "showLess", annotation = "showLess")
append(showLess)
pop()
}
} else {
append("$adjustedText ")
withStyle(
style = SpanStyle(
color = MaterialTheme.colors.onSurface,
fontSize = 14.sp,
)
) {
if (showOverflow.value) {
pushStringAnnotation(tag = "showMore", annotation = "showMore")
append(showMore)
pop()
}
}
}
}
Box(modifier = modifier) {
ClickableText(
text = annotatedText,
style = (MaterialTheme.typography.body1.copy(color = clickColor, textAlign = textAlign)),
maxLines = if (isExpanded) Int.MAX_VALUE else MINIMIZED_MAX_LINES,
onTextLayout = { textLayoutResultState = it },
onClick = { offset ->
annotatedText.getStringAnnotations(
tag = "showLess",
start = offset,
end = offset + showLess.length
).firstOrNull()?.let {
isExpanded = !isExpanded
}
annotatedText.getStringAnnotations(
tag = "showMore",
start = offset,
end = offset + showMore.length
).firstOrNull()?.let {
isExpanded = !isExpanded
}
}
)
}
}
所以我正在使用 Text()
可组合项,如下所示:
Text(
text = "this is some sample text that is long and so it is
ellipsized",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
它正确地省略了文本:
问题是我想要在省略号末尾添加一个 See More
标记,提示用户展开可见的文本框。我将如何添加它?
要解决此问题,您需要使用 onTextLayout
获取 TextLayoutResult
:它包含有关绘制文本状态的所有信息。
让它适用于多行是一项棘手的任务。为此,您需要计算椭圆文本和“...查看更多”文本的大小,然后,当您拥有这两个值时,您需要计算需要删除多少文本,以便“...查看更多”非常适合在行尾:
@Composable
fun ExpandableText(
text: String,
modifier: Modifier = Modifier,
minimizedMaxLines: Int = 1,
) {
var cutText by remember(text) { mutableStateOf<String?>(null) }
var expanded by remember { mutableStateOf(false) }
val textLayoutResultState = remember { mutableStateOf<TextLayoutResult?>(null) }
val seeMoreSizeState = remember { mutableStateOf<IntSize?>(null) }
val seeMoreOffsetState = remember { mutableStateOf<Offset?>(null) }
// getting raw values for smart cast
val textLayoutResult = textLayoutResultState.value
val seeMoreSize = seeMoreSizeState.value
val seeMoreOffset = seeMoreOffsetState.value
LaunchedEffect(text, expanded, textLayoutResult, seeMoreSize) {
val lastLineIndex = minimizedMaxLines - 1
if (!expanded && textLayoutResult != null && seeMoreSize != null
&& lastLineIndex + 1 == textLayoutResult.lineCount
&& textLayoutResult.isLineEllipsized(lastLineIndex)
) {
var lastCharIndex = textLayoutResult.getLineEnd(lastLineIndex, visibleEnd = true) + 1
var charRect: Rect
do {
lastCharIndex -= 1
charRect = textLayoutResult.getCursorRect(lastCharIndex)
} while (
charRect.left > textLayoutResult.size.width - seeMoreSize.width
)
seeMoreOffsetState.value = Offset(charRect.left, charRect.bottom - seeMoreSize.height)
cutText = text.substring(startIndex = 0, endIndex = lastCharIndex)
}
}
Box(modifier) {
Text(
text = cutText ?: text,
maxLines = if (expanded) Int.MAX_VALUE else minimizedMaxLines,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textLayoutResultState.value = it },
)
if (!expanded) {
val density = LocalDensity.current
Text(
"... See more",
onTextLayout = { seeMoreSizeState.value = it.size },
modifier = Modifier
.then(
if (seeMoreOffset != null)
Modifier.offset(
x = with(density) { seeMoreOffset.x.toDp() },
y = with(density) { seeMoreOffset.y.toDp() },
)
else
Modifier
)
.clickable {
expanded = true
cutText = null
}
.alpha(if (seeMoreOffset != null) 1f else 0f)
)
}
}
}
@Composable
fun ExpandedText(
text: String,
expandedText: String,
expandedTextButton: String,
shrinkTextButton: String,
modifier: Modifier = Modifier,
softWrap: Boolean = true,
textStyle: TextStyle = LocalTextStyle.current,
expandedTextStyle: TextStyle = LocalTextStyle.current,
expandedTextButtonStyle: TextStyle = LocalTextStyle.current,
shrinkTextButtonStyle: TextStyle = LocalTextStyle.current,
) {
var isExpanded by remember { mutableStateOf(false) }
val textHandler = "${if (isExpanded) expandedText else text} ${if (isExpanded) shrinkTextButton else expandedTextButton}"
val annotatedString = buildAnnotatedString {
withStyle(
if (isExpanded) expandedTextStyle.toSpanStyle() else textStyle.toSpanStyle()
) {
append(if (isExpanded) expandedText else text)
}
append(" ")
withStyle(
if (isExpanded) shrinkTextButtonStyle.toSpanStyle() else expandedTextButtonStyle.toSpanStyle()
) {
append(if (isExpanded) shrinkTextButton else expandedTextButton)
}
addStringAnnotation(
tag = "expand_shrink_text_button",
annotation = if (isExpanded) shrinkTextButton else expandedTextButton,
start = textHandler.indexOf(if (isExpanded) shrinkTextButton else expandedTextButton),
end = textHandler.indexOf(if (isExpanded) shrinkTextButton else expandedTextButton) + if (isExpanded) expandedTextButton.length else shrinkTextButton.length
)
}
ClickableText(
text = annotatedString,
softWrap = softWrap,
modifier = modifier,
onClick = {
annotatedString
.getStringAnnotations(
"expand_shrink_text_button",
it,
it
)
.firstOrNull()?.let { stringAnnotation ->
isExpanded = stringAnnotation.item == expandedTextButton
}
}
)
}
用法
ExpandedText(
text = food.content,
expandedText = food.contentFull,
expandedTextButton = " more",
shrinkTextButton = " less",
textStyle = typographySkModernist().body1.copy(
color = black.copy(alpha = 0.8f)
),
expandedTextStyle = typographySkModernist().body1.copy(
color = black.copy(alpha = 0.8f)
),
expandedTextButtonStyle = typographySkModernist().body1.copy(
color = orange,
),
shrinkTextButtonStyle = typographySkModernist().body1.copy(
color = orange,
),
modifier = Modifier
.padding(top = 32.dp, start = 24.dp, end = 16.dp)
)
我发现发布的解决方案有些矫枉过正。这是一个简单的解决方案:
var showMore by remember { mutableStateOf(false) }
val text =
"Space Exploration Technologies Corp. (doing business as SpaceX) is an American aerospace manufacturer, space transportation services and communications corporation headquartered in Hawthorne, California. SpaceX was founded in 2002 by Elon Musk with the goal of reducing space transportation costs to enable the colonization of Mars. SpaceX manufactures the Falcon 9 and Falcon Heavy launch vehicles, several rocket engines, Cargo Dragon, crew spacecraft and Starlink communications satellites."
Column(modifier = Modifier.padding(20.dp)) {
Column(modifier = Modifier
.animateContentSize(animationSpec = tween(100))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { showMore = !showMore }) {
if (showMore) {
Text(text = text)
} else {
Text(text = text, maxLines = 3, overflow = TextOverflow.Ellipsis)
}
}
}
一个简单的实现:
@Composable
fun ExpandableText(
modifier: Modifier = Modifier,
text: String,
minimizedMaxLines: Int,
style: TextStyle
) {
var expanded by remember { mutableStateOf(false) }
var hasVisualOverflow by remember { mutableStateOf(false) }
Box(modifier = modifier) {
Text(
text = text,
maxLines = if (expanded) Int.MAX_VALUE else minimizedMaxLines,
onTextLayout = { hasVisualOverflow = it.hasVisualOverflow },
style = style
)
if (hasVisualOverflow) {
Row(
modifier = Modifier.align(Alignment.BottomEnd),
verticalAlignment = Alignment.Bottom
) {
val lineHeightDp: Dp = with(LocalDensity.current) { style.lineHeight.toDp() }
Spacer(
modifier = Modifier
.width(48.dp)
.height(lineHeightDp)
.background(
brush = Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.White)
)
)
)
Text(
modifier = Modifier
.background(Color.White)
.padding(start = 4.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { expanded = !expanded }
),
text = "Show More",
color = MaterialTheme.colors.primary,
style = style
)
}
}
}
}
我想要一个更灵活的
/**
* @param modifier use this to add padding and such
* @param longText is the Text that is to long and need to be displayed that has more than [minimizedMaxLines]
* @param minimizedMaxLines (optional) the minimum amount of text lines to be visible in non-expanded state
* @param textAlign (optional) defaults to [TextAlign.Start] unless overridden, try [TextAlign.Justify]
* @param expandHint (optional) this text is appended to the [longText] before expanding and become clickable
* @param shrinkHint (optional) this text is appended to the [longText] after expanding and become clickable
* @param clickColor (optional) denotes the color of the clickable [expandHint] & [shrinkHint] strings
* */
@Composable
fun AppExpandingText(
modifier: Modifier = Modifier,
longText: String,
minimizedMaxLines: Int = 3,
textAlign: TextAlign = TextAlign.Start,
expandHint: String = "… Show More",
shrinkHint: String = "… Show Less",
clickColor: Color = Color.Unspecified
) {
var isExpanded by remember { mutableStateOf(value = false) }
var textLayoutResultState by remember { mutableStateOf<TextLayoutResult?>(value = null) }
var adjustedText by remember { mutableStateOf(value = longText) }
val overflow = textLayoutResultState?.hasVisualOverflow ?: false
val showOverflow = remember { mutableStateOf(value = false) }
val showMore = " $expandHint"
val showLess = " $shrinkHint"
LaunchedEffect(textLayoutResultState) {
if (textLayoutResultState == null) return@LaunchedEffect
if (!isExpanded && overflow) {
showOverflow.value = true
val lastCharIndex = textLayoutResultState!!.getLineEnd(lineIndex = minimizedMaxLines - 1)
adjustedText = longText
.substring(startIndex = 0, endIndex = lastCharIndex)
.dropLast(showMore.length)
.dropLastWhile { it == ' ' || it == '.' }
}
}
val annotatedText = buildAnnotatedString {
if (isExpanded) {
append("$longText ")
withStyle(
style = SpanStyle(
color = MaterialTheme.colors.onSurface,
fontSize = 14.sp,
)
) {
pushStringAnnotation(tag = "showLess", annotation = "showLess")
append(showLess)
pop()
}
} else {
append("$adjustedText ")
withStyle(
style = SpanStyle(
color = MaterialTheme.colors.onSurface,
fontSize = 14.sp,
)
) {
if (showOverflow.value) {
pushStringAnnotation(tag = "showMore", annotation = "showMore")
append(showMore)
pop()
}
}
}
}
Box(modifier = modifier) {
ClickableText(
text = annotatedText,
style = (MaterialTheme.typography.body1.copy(color = clickColor, textAlign = textAlign)),
maxLines = if (isExpanded) Int.MAX_VALUE else MINIMIZED_MAX_LINES,
onTextLayout = { textLayoutResultState = it },
onClick = { offset ->
annotatedText.getStringAnnotations(
tag = "showLess",
start = offset,
end = offset + showLess.length
).firstOrNull()?.let {
isExpanded = !isExpanded
}
annotatedText.getStringAnnotations(
tag = "showMore",
start = offset,
end = offset + showMore.length
).firstOrNull()?.let {
isExpanded = !isExpanded
}
}
)
}
}