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

- 6,091
- 5
- 54
- 79
3 Answers
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:
- Make sure the ellipsis is necessary, given all the modifiers applied to the text.
- Make the size of the left and right parts as close to each other as possible, based on the size of the characters, not just their number.
@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:

- 67,741
- 15
- 184
- 220
-
Thats a lot of code. I hope they will provide this method soon. – Rafael Sep 07 '21 at 12:27
-
1@Rafael sure that is. If the solution was easy, they would have implemented it by now. And when they do, this question will become meaningless=) – Phil Dukhov Sep 07 '21 at 12:57
-
Worth mentioning the code dos no relayout on resize of the element, only on text change. – MrStahlfelge May 11 '22 at 08:28
-
3@MrStahlfelge I've updated my answer to support size change – Phil Dukhov May 11 '22 at 09:40
-
2Wow, I just wanted to make a remark so others are prepared as I found myself wondering why it did not work and did not expect you to improve the solution. Have much thanks! – MrStahlfelge May 12 '22 at 11:53
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" }
)

- 6,958
- 2
- 31
- 42
-
This will only with Compose for Android. Won't work with Compose for Desktop. – vovahost Oct 02 '22 at 19:08
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

- 2,377
- 7
- 20
- 39
-
1Currently we use similar approach to handle long text. `"${title.take(characterCount)}...${title.takeLast(characterCount)}"` . Since our text vary in length we pre-check its length before running this function. – Rafael Sep 07 '21 at 12:30
-
Yes it's fine, with my code you will always have two half of the word because you `count()` the characters and then divide by 2. How do you handle an odd number of characters with your code? Anyway I hope they add something soon, star the [issue](https://issuetracker.google.com/issues/185418980) posted by Philip – Stefano Sansone Sep 07 '21 at 12:37
-