0

I have a Text object in Jetpack Compose whose content is continuously getting longer, but I only want to render the last 3 lines at any given time. Essentially, I'm hoping to mimic the behavior of TextView's android:gravity="bottom", as seen here. How would I go about achieving this?

Quontas
  • 400
  • 1
  • 3
  • 19

1 Answers1

2

Text has onTextLayout parameter with will give you all needed information about the text layout. You can get the number of lines and the offset for the needed line index.

Using SubcomposeLayout you can calculate the needed offset using value got from onTextLayout before any drawing and apply it. And you need to apply Modifier.clipToBounds to prevent redundant text to be drawn.

@Composable
fun LimitedText(
    text: String,
    maxBottomLines: Int,
    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,
    style: TextStyle = LocalTextStyle.current,
) {
    SubcomposeLayout(
        // prevent offset text from being drawn
        modifier.clipToBounds()
    ) { constraints ->
        var extraLinesHeight = 0
        val placeable = subcompose(null) {
            Text(
                text = text,
                color = color,
                fontSize = fontSize,
                fontStyle = fontStyle,
                fontWeight = fontWeight,
                fontFamily = fontFamily,
                letterSpacing = letterSpacing,
                textDecoration = textDecoration,
                textAlign = textAlign,
                lineHeight = lineHeight,
                overflow = overflow,
                softWrap = softWrap,
                onTextLayout = { textLayoutResult ->
                    val extraLines = textLayoutResult.lineCount - maxBottomLines
                    if (extraLines > 0) {
                        extraLinesHeight = textLayoutResult.getLineTop(extraLines).roundToInt()
                    }
                },
                style = style,
            )
        }[0].measure(
            // override maxWidth to get full text size
            constraints.copy(maxHeight = Int.MAX_VALUE)
        )
        layout(
            width = placeable.width,
            height = placeable.height - extraLinesHeight
        ) {
            placeable.place(0, -extraLinesHeight)
        }
    }
}

Usage:

LimitedText(
    LoremIpsum().values.first(),
    maxBottomLines = 3,
)

Most of time this would be enough, but in case you will need to get the final result of onTextLayout, you can calculate the text offset and add one more placeable with subcompose which will contain only the needed part of the text, and place only the second placeable.

@Composable
fun LimitedText(
    text: String,
    maxBottomLines: Int,
    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,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current,
) {
    SubcomposeLayout(modifier) { constraints ->
        var slotId = 0
        fun placeText(
            text: String,
            onTextLayout: (TextLayoutResult) -> Unit,
            constraints: Constraints,
        ) = subcompose(slotId++) {
            Text(
                text = text,
                color = color,
                fontSize = fontSize,
                fontStyle = fontStyle,
                fontWeight = fontWeight,
                fontFamily = fontFamily,
                letterSpacing = letterSpacing,
                textDecoration = textDecoration,
                textAlign = textAlign,
                lineHeight = lineHeight,
                overflow = overflow,
                softWrap = softWrap,
                onTextLayout = onTextLayout,
                style = style,
            )
        }[0].measure(constraints)
        var substringStartIndex: Int? = null
        val initialPlaceable = placeText(
            text = text,
            // override maxWidth to get full text size
            constraints = constraints.copy(maxHeight = Int.MAX_VALUE),
            onTextLayout = { textLayoutResult ->
                val extraLines = textLayoutResult.lineCount - maxBottomLines
                if (extraLines > 0) {
                    substringStartIndex = textLayoutResult.run {
                        getOffsetForPosition(
                            Offset(1f, getLineTop(extraLines) + 1f)
                        )
                    }
                } else {
                    onTextLayout(textLayoutResult)
                }
            },
        )
        val finalPlaceable = substringStartIndex?.let {
            placeText(
                text = text.substring(startIndex = it),
                constraints = constraints,
                onTextLayout = onTextLayout,
            )
        } ?: initialPlaceable

        layout(
            width = finalPlaceable.width,
            height = finalPlaceable.height
        ) {
            finalPlaceable.place(0, 0)
        }
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220