Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 420 Vote(s) - 3.51 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Expandable Text in Jetpack Compose

#1
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:

[![enter image description here][1]][1]

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?

[![enter image description here][2]][2]


[1]:

[2]:
Reply

#2
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)
)
}
}
}
```

<img src="https://i.stack.imgur.com/4pk56.gif" width="300"></img>
Reply

#3
@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)
)

[![enter image description here][1]][1]


[1]:
Reply

#4
I found the posted solutions kind of overkill. Here's a simple solution:

```kotlin
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)
}
}
}
```
Reply

#5
A simple implementation:

[![enter image description here][1]][1]

```
@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
)
}
}
}
}
```


[1]:
Reply

#6
I wanted a more Flexible one
```kotlin
package {packageName}.core.presentation.components

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
import Constants.MINIMIZED_MAX_LINES

/**
* @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)
addStyle(
style = SpanStyle(
color = clickColor,
fontSize = 14.sp
),
start = longText.length,
end = longText.length + showMore.length
)
pop()
}
} else {
append(adjustedText)
withStyle(
style = SpanStyle(
color = MaterialTheme.colors.onSurface,
fontSize = 14.sp
)
) {
if (showOverflow.value) {
pushStringAnnotation(tag = "showMore", annotation = "showMore")
append(showMore)
addStyle(
style = SpanStyle(
color = clickColor,
fontSize = 14.sp
),
start = adjustedText.length,
end = adjustedText.length + showMore.length
)
pop()
}
}
}

}
Box(modifier = modifier) {
ClickableText(
text = annotatedText,
style = (MaterialTheme.typography.body1.copy(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
}
}
)
}
}
```
**Sample:**

[![enter image description here][1]][1]
[![enter image description here][2]][2]


[1]:

[2]:
Reply

#7
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
)
}

}
```

<img src="https://i.stack.imgur.com/72jjr.gif" width="256">
Reply

#8
I have an implementation [here][1].
Like others have said, we should use onTextLayout to grab the necessary measurements like text width, etc. In my example, I tried to minimize the recomposition by remembering necessary values

Recomposition and skip count:


[1]:

[To see links please register here]

Reply

#9
rewritten it

[To see links please register here]

Click area only the word "... More"

@Composable
fun ExpandableText(
modifier: Modifier = Modifier,
textModifier: Modifier = Modifier,
style: TextStyle = MaterialTheme.typography.body2,
color: Color = textLightPrimary,
text: String,
collapsedMaxLine: Int = 6,
showMoreText: String = "... More",
showMoreStyle: SpanStyle = SpanStyle(
fontWeight = FontWeight.Bold, textDecoration = TextDecoration.Underline, color = color
),
) {
var isExpanded by remember { mutableStateOf(false) }
var clickable by remember { mutableStateOf(false) }
var lastCharIndex by remember { mutableStateOf(0) }

val textSpanStyle = style.toSpanStyle().copy(color = color)
Box(
modifier = Modifier.then(modifier)
) {
val annotatedString = buildAnnotatedString {
if (clickable) {
if (isExpanded) {
withStyle(style = textSpanStyle) { append(text) }
} else {
val adjustText =
text.substring(startIndex = 0, endIndex = lastCharIndex).dropLast(showMoreText.length)
.dropLastWhile { Character.isWhitespace(it) || it == '.' }
withStyle(style = textSpanStyle) { append(adjustText) }
pushStringAnnotation(tag = "MORE", annotation = showMoreText)
withStyle(style = showMoreStyle) { append(showMoreText) }
}
} else {
withStyle(style = textSpanStyle) { append(text) }
}
}
ClickableText(modifier = textModifier
.fillMaxWidth()
.animateContentSize(),
text = annotatedString,
maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLine,
onTextLayout = { textLayoutResult ->
if (!isExpanded && textLayoutResult.hasVisualOverflow) {
clickable = true
lastCharIndex = textLayoutResult.getLineEnd(collapsedMaxLine - 1)
}
},
style = style,
onClick = {
annotatedString.getStringAnnotations("MORE", it, it).firstOrNull()
?.let { more -> isExpanded = !isExpanded }
})
}
}
Reply

#10
Here is a simplified approach that works for me. It recompiles 2x on initialization to update the text and only once when clicked.

Model:

data class PostExpandableDescriptionModel(
var maxLine: Int,
var modifiedText: String? )

Composable:

@Composable
fun PostDescription(
descriptionString: String,
maxLine: Int
){
// Saves the description and triggers recomposition on change
val description = remember {
mutableStateOf(descriptionString)
}

// Saves the data model
val model = remember {
mutableStateOf( PostExpandableDescriptionModel(
maxLine = maxLine,
modifiedText = null )
)
}
// See more text
val seeMoreText = "... See more"

Box(
modifier = Modifier
.padding((6.3).dp)
.clickable {
// Clickable box for easier user interaction
when( model.value.maxLine ) {
maxLine -> {
model.value.maxLine = Int.MAX_VALUE
description.value = descriptionString
}
else -> {
model.value.maxLine = maxLine
model.value.modifiedText?.let {
description.value = it
}
}
}
}
){

Text(
text = description.value,
maxLines = model.value.maxLine,
onTextLayout = { textLayoutResult ->
//Saves the modified text only once
if( textLayoutResult.hasVisualOverflow && model.value.modifiedText.isNullOrEmpty()){
val lineEndOffset = textLayoutResult.getLineEnd(maxLine - 1)
val newString = descriptionString.substring(0, lineEndOffset - seeMoreText.length)
model.value.modifiedText = (newString + seeMoreText).also {
description.value = it
}
}
}
)
}
}
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through