so I am using a Text()
composable like so:
Text(
text = "this is some sample text that is long and so it is
ellipsized",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
and it ellipsizes the text properly:
The issue is that I want a See More
tag at the end of the ellipsis, prompting the user to expand the visible text box. How would I go about adding that?
To read value entered in TextField in Android Compose, declare a variable, and assign this variable the value in the TextField whenever there is a change in the value. The same process holds for reading value entered in OutlineTextField composable.
To center align content of Column along horizontal axis in Android Compose, set horizontalAlignment parameter with the value of Alignment. CenterHorizontally . Also, we may fill the maximum width by the Column using Modifier. fillMaxWidth().
Jetpack Compose is a modern toolkit designed to simplify UI development. It combines a reactive programming model with the conciseness and ease of use of the Kotlin programming language. It is fully declarative, meaning you describe your UI by calling a series of functions that transform data into a UI hierarchy.
By default, composables aren’t selectable, which means by default users can't select and copy text from your app. To enable text selection, you need to wrap your text elements with a SelectionContainer composable: You may want to disable selection on specific parts of a selectable area.
To do so, set the textOverflow parameter: includeFontPadding is a legacy property that adds extra padding based on font metrics at the top of the first line and bottom of the last line of a text. In Compose 1.2.0, includeFontPadding is set to true by default.
If you want to set manually the text alignment of a Text composable, prefer using TextAlign.Start and TextAlign.End instead of TextAlign.Left and TextAlign.Right respectively, as they resolve to the right edge of the Text composable depending on the preferred language text orientation.
So we can take a text (String) and append to it the Strings of “Show Less/…Show More) if the total text is going to expand or not. Then after each click we are going to toggle this functionality (implemented in the ClickableText).
To solve this you need to use onTextLayout
to get TextLayoutResult
: it contains all info about the state of drawn text.
Making it work for multiple lines is a tricky task. To do that you need to calculate sizes of both ellipsized text and "... See more" text, then, when you have both values you need to calculate how much text needs to be removed so "... See more" fits perfectly at the end of line:
@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
}
}
)
}
usage
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)
)
I found the posted solutions kind of overkill. Here's a simple solution:
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)
}
}
}
A simple implementation:
@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
)
}
}
}
}
My simple implementation, hope it useful:
const val DEFAULT_MINIMUM_TEXT_LINE = 3
@Composable
fun ExpandableText(
modifier: Modifier = Modifier,
textModifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
fontStyle: FontStyle? = null,
text: String,
collapsedMaxLine: Int = DEFAULT_MINIMUM_TEXT_LINE,
showMoreText: String = "... Show More",
showMoreStyle: SpanStyle = SpanStyle(fontWeight = FontWeight.W500),
showLessText: String = " Show Less",
showLessStyle: SpanStyle = showMoreStyle,
textAlign: TextAlign? = null
) {
var isExpanded by remember { mutableStateOf(false) }
var clickable by remember { mutableStateOf(false) }
var lastCharIndex by remember { mutableStateOf(0) }
Box(modifier = Modifier
.clickable(clickable) {
isExpanded = !isExpanded
}
.then(modifier)
) {
Text(
modifier = textModifier
.fillMaxWidth()
.animateContentSize(),
text = buildAnnotatedString {
if (clickable) {
if (isExpanded) {
append(text)
withStyle(style = showLessStyle) { append(showLessText) }
} else {
val adjustText = text.substring(startIndex = 0, endIndex = lastCharIndex)
.dropLast(showMoreText.length)
.dropLastWhile { Character.isWhitespace(it) || it == '.' }
append(adjustText)
withStyle(style = showMoreStyle) { append(showMoreText) }
}
} else {
append(text)
}
},
maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLine,
fontStyle = fontStyle,
onTextLayout = { textLayoutResult ->
if (!isExpanded && textLayoutResult.hasVisualOverflow) {
clickable = true
lastCharIndex = textLayoutResult.getLineEnd(collapsedMaxLine - 1)
}
},
style = style,
textAlign = textAlign
)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With