I need to make Middle Ellipsis in Jetpack Compose Text. As far as I see there is only Clip, Ellipsis and Visible options for TextOverflow.
Something like this: 4gh45g43h...bh4bh6b64
I need to make Middle Ellipsis in Jetpack Compose Text. As far as I see there is only Clip, Ellipsis and Visible options for TextOverflow.
Something like this: 4gh45g43h...bh4bh6b64
It is not officially supported yet, keep an eye on this issue.
For now, you can use the following method. I use SubcomposeLayout to get onTextLayout result without actually drawing the initial text.
It takes so much code and calculations to:
@Composable
fun MiddleEllipsisText(
text: String,
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,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
// some letters, like "r", will have less width when placed right before "."
// adding a space to prevent such case
val layoutText = remember(text) { "$text $ellipsisText" }
val textLayoutResultState = remember(layoutText) {
mutableStateOf<TextLayoutResult?>(null)
}
SubcomposeLayout(modifier) { constraints ->
// result is ignored - we only need to fill our textLayoutResult
subcompose("measure") {
Text(
text = layoutText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
maxLines = 1,
onTextLayout = { textLayoutResultState.value = it },
style = style,
)
}.first().measure(Constraints())
// to allow smart cast
val textLayoutResult = textLayoutResultState.value
?: // shouldn't happen - onTextLayout is called before subcompose finishes
return@SubcomposeLayout layout(0, 0) {}
val placeable = subcompose("visible") {
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
if (text.isEmpty() || textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth) {
// text not including ellipsis fits on the first line.
return@remember text
}
val ellipsisWidth = layoutText.indices.toList()
.takeLast(ellipsisCharactersCount)
.let widthLet@{ indices ->
// fix this bug: https://issuetracker.google.com/issues/197146630
// in this case width is invalid
for (i in indices) {
val width = textLayoutResult.getBoundingBox(i).width
if (width > 0) {
return@widthLet width * ellipsisCharactersCount
}
}
// this should not happen, because
// this error occurs only for the last character in the string
throw IllegalStateException("all ellipsis chars have invalid width")
}
val availableWidth = constraints.maxWidth - ellipsisWidth
val startCounter = BoundCounter(text, textLayoutResult) { it }
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
while (availableWidth - startCounter.width - endCounter.width > 0) {
val possibleEndWidth = endCounter.widthWithNextChar()
if (
startCounter.width >= possibleEndWidth
&& availableWidth - startCounter.width - possibleEndWidth >= 0
) {
endCounter.addNextChar()
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
startCounter.addNextChar()
} else {
break
}
}
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
}
Text(
text = finalText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
onTextLayout = onTextLayout,
style = style,
)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private const val ellipsisCharactersCount = 3
private const val ellipsisCharacter = '.'
private val ellipsisText = List(ellipsisCharactersCount) { ellipsisCharacter }.joinToString(separator = "")
private class BoundCounter(
private val text: String,
private val textLayoutResult: TextLayoutResult,
private val charPosition: (Int) -> Int,
) {
var string = ""
private set
var width = 0f
private set
private var _nextCharWidth: Float? = null
private var invalidCharsCount = 0
fun widthWithNextChar(): Float =
width + nextCharWidth()
private fun nextCharWidth(): Float =
_nextCharWidth ?: run {
var boundingBox: Rect
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
invalidCharsCount--
do {
boundingBox = textLayoutResult
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
} while (boundingBox.right == 0f)
_nextCharWidth = boundingBox.width
boundingBox.width
}
fun addNextChar() {
string += text[charPosition(string.count())]
width += nextCharWidth()
_nextCharWidth = null
}
}
My testing code:
val text = remember { LoremIpsum(100).values.first().replace("\n", " ") }
var length by remember { mutableStateOf(77) }
var width by remember { mutableStateOf(0.5f) }
Column {
MiddleEllipsisText(
text.take(length),
fontSize = 30.sp,
modifier = Modifier
.background(Color.LightGray)
.padding(10.dp)
.fillMaxWidth(width)
)
Slider(
value = length.toFloat(),
onValueChange = { length = it.roundToInt() },
valueRange = 2f..text.length.toFloat()
)
Slider(
value = width,
onValueChange = { width = it },
)
}
Result:

There is currently no specific function in Compose yet.
A possible approach is to process the string yourself before using it, with the kotlin functions.
val word = "4gh45g43hbh4bh6b64" //put your string here
val chunks = word.chunked((word.count().toDouble()/2).roundToInt())
val midEllipsis = "${chunks[0]}…${chunks[1]}"
println(midEllipsis)
I use the chunked function to divide the string into an array of strings, which will always be two because as a parameter I give it the size of the string divided by 2 and rounded up.
Result : 4gh45g43h…bh4bh6b64
To use the .roundToInt() function you need the following import
import kotlin.math.roundToInt
Since TextView already supports ellipsize in the middle you can just wrap it in compose using AndroidView
AndroidView(
factory = { context ->
TextView(context).apply {
maxLines = 1
ellipsize = MIDDLE
}
},
update = { it.text = "A looooooooooong text" }
)