Andrey
Andrey

Reputation: 815

Show inline content with Ellipsis overflow in Jetpack Compose Text doesn't work

Tried to implement Text with timestamp. Found that easiest way it's use inlineContent. But it doesn't work well with overflow = TextOverflow.Ellipsis

@Composable
fun TextWithTimestamp(
    text: String,
    timestamp: String,
    maxLines: Int,
    modifier: Modifier = Modifier,
) {

    val annotatedString = buildAnnotatedString {
        withStyle(style = SpanStyle(color = Color.White)) {
            append(text)
        }
        pushStyle(SpanStyle(fontSize = 14.sp))
        appendInlineContent("timestamp", timestamp)
        pop()
    }

    val inlineContent = remember(text) {
        mutableMapOf(
            "timestamp" to InlineTextContent(
                placeholder = Placeholder(
                    height = 20.sp,
                    width = 20.sp,
                    placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
                ),
                children = {
                    Text(
                        text = it,
                        color = Color.Yellow,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            )
        )
    }

    Text(
        text = annotatedString,
        inlineContent = inlineContent,
        maxLines = maxLines,
        overflow = TextOverflow.Ellipsis,
    )
}

@Preview(widthDp = 200)
@Composable
private fun PreviewTextWithTimestamp() {
    Column(
        modifier = Modifier
            .padding(16.dp)
            .border(1.dp, Color.Green)
    ) {
        TextWithTimestamp(
            text = "MedalParrot, MedalPanda, MedalCarrot and 83+ liked your clip!",
            timestamp = "1h",
            maxLines = 2,
        )
        Divider(color = Color.Green)
        TextWithTimestamp(
            text = "MedalParrot, MedalPanda, MedalCarrot and 83+ liked your clip!",
            timestamp = "1h",
            maxLines = 3,
        )
    }
}

Without overflow, it works as expected, but with TextOverflow.Ellipsis, only ... is shown, but inline content doesn't.

In design, I have limitation of content width and defined max lines. The text can be any length:

Preview with maxLines = 2 and maxLines = 3

Upvotes: 4

Views: 751

Answers (1)

Fartab
Fartab

Reputation: 5513

You can implement your own truncation logic by using onTextLayout callback.

The truncation logic in this code snippet aims to ensure that the displayed text fits within the specified maximum number of lines. It achieves this by iteratively removing characters from the end of the text and appending an ellipsis ("…") until the text no longer overflows. The onTextLayout callback detects overflow and triggers a recomposition with an incremented truncationCount, leading to a shorter version of the text being displayed. This process repeats until the text fits within the allowed lines, at which point the final truncated version is rendered.

The shouldDraw variable acts as a control mechanism to ensure that the text content is only rendered when it's fully truncated and fits within the specified maximum lines. It prevents intermediate, partially truncated versions from being displayed during the truncation process, resulting in a smoother user experience by only showing the final, correctly truncated text.

@Composable
fun TextWithTimestamp(
    text: String,
    timestamp: String,
    maxLines: Int,
    modifier: Modifier = Modifier,
) {

    val isInPreviewMode = LocalInspectionMode.current
    var shouldDraw by remember { mutableStateOf(false) }
    var truncationCount by remember { mutableStateOf(0) }
    val textToShow by remember(truncationCount) {
        derivedStateOf {
            if (truncationCount == 0) text
            else "${text.dropLast(truncationCount)}…"
        }
    }


    val annotatedString = buildAnnotatedString {
        withStyle(style = SpanStyle(color = Color.White)) {
            append(textToShow)
        }
        pushStyle(SpanStyle(fontSize = 14.sp))
        appendInlineContent("timestamp", timestamp)
        pop()
    }

    val inlineContent = remember(text) {
        mutableMapOf(
            "timestamp" to InlineTextContent(
                placeholder = Placeholder(
                    height = 20.sp,
                    width = 20.sp,
                    placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
                ),
                children = {
                    Text(
                        text = it,
                        color = Color.Yellow,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            )
        )
    }

    Text(
        modifier = Modifier.drawWithContent { if (shouldDraw || isInPreviewMode) drawContent() },
        text = annotatedString,
        inlineContent = inlineContent,
        maxLines = maxLines,
        overflow = TextOverflow.Clip,
        onTextLayout = { result ->
            if (result.hasVisualOverflow) {
                truncationCount++
                shouldDraw = false
            } else {
                shouldDraw = true
            }
        },
    )
}

@Preview(widthDp = 200)
@Composable
private fun PreviewTextWithTimestamp() {
    Column(
        modifier = Modifier
            .background(Color.Black)
            .width(200.dp)
            .padding(16.dp)
            .border(1.dp, Color.Green)
    ) {
        TextWithTimestamp(
            text = "MedalParrot, MedalPanda, MedalCarrot and 83+ liked your clip!",
            timestamp = "1h",
            maxLines = 2,
        )
        Divider(color = Color.Green)
        TextWithTimestamp(
            text = "MedalParrot, MedalPanda, MedalCarrot and 83+ liked your clip!",
            timestamp = "1h",
            maxLines = 3,
        )
    }
}

Upvotes: 0

Related Questions