简体   繁体   English

如何使用 Jetpack Compose 在文本中制作中间省略号

[英]How to make middle ellipsis in Text with Jetpack Compose

I need to make Middle Ellipsis in Jetpack Compose Text.我需要在 Jetpack Compose Text 中制作中间省略号。 As far as I see there is only Clip, Ellipsis and Visible options for TextOverflow.据我所知,TextOverflow 只有 Clip、Ellipsis 和 Visible 选项。 Something like this: 4gh45g43h...bh4bh6b64像这样的东西: 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.我使用SubcomposeLayout来获得onTextLayout结果,而无需实际绘制初始文本。

It takes so much code and calculations to:需要大量代码和计算才能:

  1. Make sure the ellipsis is necessary, given all the modifiers applied to the text.考虑到应用于文本的所有修饰符,请确保省略号是必要的。
  2. 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:结果:

There is currently no specific function in Compose yet. Compose 目前还没有具体的功能。
A possible approach is to process the string yourself before using it, with the kotlin functions.一种可能的方法是在使用之前使用 kotlin 函数自己处理字符串。

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.我使用chunked函数将字符串划分为字符串数组,该数组始终为 2,因为作为参数,我将字符串的大小除以 2 并四舍五入。

Result : 4gh45g43h…bh4bh6b64结果4gh45g43h…bh4bh6b64

To use the .roundToInt() function you need the following import要使用.roundToInt()函数,您需要以下导入

import kotlin.math.roundToInt

Test this yourself in the playground在操场上自己测试一下

Since TextView already supports ellipsize in the middle you can just wrap it in compose using AndroidView由于TextView已经支持中间的 ellipsize,您可以使用AndroidView将其包装在 compose 中

AndroidView(
  factory = { context ->
    TextView(context).apply {
      maxLines = 1
      ellipsize = MIDDLE
    }
  },
  update = { it.text = "A looooooooooong text" }
)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM