android:Jetpack Compose 中的 autoSizeTextType

问题描述 投票:0回答:14

有没有办法调整文本以始终根据固定高度调整大小?

我有一个高度固定的列,里面的文字应该总是适合

 Column(modifier = Modifier.height(150.dp).padding(8.dp)) {
   Text("My really long long long long long text that needs to be resized to the height of this Column")
}
android kotlin android-jetpack android-jetpack-compose
14个回答
41
投票

我使用以下内容根据可用宽度调整字体大小:

val textStyleBody1 = MaterialTheme.typography.body1
var textStyle by remember { mutableStateOf(textStyleBody1) }
var readyToDraw by remember { mutableStateOf(false) }
Text(
    text = "long text goes here",
    style = textStyle,
    maxLines = 1,
    softWrap = false,
    modifier = modifier.drawWithContent {
        if (readyToDraw) drawContent()
    },
    onTextLayout = { textLayoutResult ->
        if (textLayoutResult.didOverflowWidth) {
            textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9)
        } else {
            readyToDraw = true
        }
    }
)

要根据高度调整字体大小,请使用

Text
可组合项的属性并使用
didOverflowHeight
而不是
didOverflowWidth

val textStyleBody1 = MaterialTheme.typography.body1
var textStyle by remember { mutableStateOf(textStyleBody1) }
var readyToDraw by remember { mutableStateOf(false) }
Text(
    text = "long text goes here",
    style = textStyle,
    overflow = TextOverflow.Clip,
    modifier = modifier.drawWithContent {
        if (readyToDraw) drawContent()
    },
    onTextLayout = { textLayoutResult ->
        if (textLayoutResult.didOverflowHeight) {
            textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9)
        } else {
            readyToDraw = true
        }
    }
)

如果您需要同步列表中多个项目的字体大小,请将文本样式保存在可组合函数之外:

private val textStyle = mutableStateOf(MaterialTheme.typography.body1)

@Composable
fun YourComposable() {
    Text(...)
}

这当然不是完美的,因为它可能需要一些帧才能适合大小并最终绘制文本。


27
投票

我建立在 Brian 的回答 之上以支持 Text 的其他属性,这些属性也被提升并且可以被调用者使用。

@Composable
fun AutoResizeText(
    text: String,
    fontSizeRange: FontSizeRange,
    modifier: Modifier = Modifier,
    color: Color = Color.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,
    maxLines: Int = Int.MAX_VALUE,
    style: TextStyle = LocalTextStyle.current,
) {
    var fontSizeValue by remember { mutableStateOf(fontSizeRange.max.value) }
    var readyToDraw by remember { mutableStateOf(false) }

    Text(
        text = text,
        color = color,
        maxLines = maxLines,
        fontStyle = fontStyle,
        fontWeight = fontWeight,
        fontFamily = fontFamily,
        letterSpacing = letterSpacing,
        textDecoration = textDecoration,
        textAlign = textAlign,
        lineHeight = lineHeight,
        overflow = overflow,
        softWrap = softWrap,
        style = style,
        fontSize = fontSizeValue.sp,
        onTextLayout = {
            Timber.d("onTextLayout")
            if (it.didOverflowHeight && !readyToDraw) {
                Timber.d("Did Overflow height, calculate next font size value")
                val nextFontSizeValue = fontSizeValue - fontSizeRange.step.value
                if (nextFontSizeValue <= fontSizeRange.min.value) {
                    // Reached minimum, set minimum font size and it's readToDraw
                    fontSizeValue = fontSizeRange.min.value
                    readyToDraw = true
                } else {
                    // Text doesn't fit yet and haven't reached minimum text range, keep decreasing
                    fontSizeValue = nextFontSizeValue
                }
            } else {
                // Text fits before reaching the minimum, it's readyToDraw
                readyToDraw = true
            }
        },
        modifier = modifier.drawWithContent { if (readyToDraw) drawContent() }
    )
}

data class FontSizeRange(
    val min: TextUnit,
    val max: TextUnit,
    val step: TextUnit = DEFAULT_TEXT_STEP,
) {
    init {
        require(min < max) { "min should be less than max, $this" }
        require(step.value > 0) { "step should be greater than 0, $this" }
    }

    companion object {
        private val DEFAULT_TEXT_STEP = 1.sp
    }
}

用法看起来像:

AutoResizeText(
    text = "Your Text",
    maxLines = 3,
    modifier = Modifier.fillMaxWidth(),
    fontSizeRange = FontSizeRange(
        min = 10.sp,
        max = 22.sp,
    ),
    overflow = TextOverflow.Ellipsis,
    style = MaterialTheme.typography.body1,
)

这样我就可以设置不同的 maxLines,甚至可以让省略号溢出,因为即使是我们想要的最小尺寸,文本太大而无法放入设置的行中。


20
投票

这是一个基于@Brian 和@zxon 评论的可组合项,可根据可用宽度自动调整文本大小。

@Composable
fun AutoSizeText(
    text: String,
    textStyle: TextStyle,
    modifier: Modifier = Modifier
) {
    var scaledTextStyle by remember { mutableStateOf(textStyle) }
    var readyToDraw by remember { mutableStateOf(false) }

    Text(
            text,
            modifier.drawWithContent {
                if (readyToDraw) {
                    drawContent()
                }
            },
            style = scaledTextStyle,
            softWrap = false,
            onTextLayout = { textLayoutResult ->
                if (textLayoutResult.didOverflowWidth) {
                    scaledTextStyle =
                            scaledTextStyle.copy(fontSize = scaledTextStyle.fontSize * 0.9)
                } else {
                    readyToDraw = true
                }
            }
    )
}

预览不能正常工作(至少对于 beta09),您可以添加此代码以使用占位符进行预览:

    if (LocalInspectionMode.current) {
        Text(
                text,
                modifier,
                style = textStyle
        )
        return
    } 

11
投票

我做了这样的事

@Composable
fun AutosizeText() {

    var multiplier by remember { mutableStateOf(1f) }

    Text(
        "Some long-ish text",
        maxLines = 1, // modify to fit your need
        overflow = TextOverflow.Visible,
        style = LocalTextStyle.current.copy(
            fontSize = LocalTextStyle.current.fontSize * multiplier
        ),
        onTextLayout = {
            if (it.hasVisualOverflow) {
                multiplier *= 0.99f // you can tune this constant 
            }
        }
    )
}

你可以直观地看到文字缩小直到适合


7
投票

与预览一起工作)这是另一种解决方案,使用

BoxWithConstraints
获取可用宽度并将其与使用
ParagraphIntrinsics
将文本排成一行所需的宽度进行比较:

@Composable
private fun AutosizeText(
    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,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    BoxWithConstraints {
        var shrunkFontSize = fontSize
        val calculateIntrinsics = @Composable {
            ParagraphIntrinsics(
                text, TextStyle(
                    color = color,
                    fontSize = shrunkFontSize,
                    fontWeight = fontWeight,
                    textAlign = textAlign,
                    lineHeight = lineHeight,
                    fontFamily = fontFamily,
                    textDecoration = textDecoration,
                    fontStyle = fontStyle,
                    letterSpacing = letterSpacing
                ),
                density = LocalDensity.current,
                resourceLoader = LocalFontLoader.current
            )
        }

        var intrinsics = calculateIntrinsics()
        with(LocalDensity.current) {
            while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
                shrunkFontSize *= 0.9
                intrinsics = calculateIntrinsics()
            }
        }
        Text(
            text = text,
            modifier = modifier,
            color = color,
            fontSize = shrunkFontSize,
            fontStyle = fontStyle,
            fontWeight = fontWeight,
            fontFamily = fontFamily,
            letterSpacing = letterSpacing,
            textDecoration = textDecoration,
            textAlign = textAlign,
            lineHeight = lineHeight,
            onTextLayout = onTextLayout,
            style = style
        )
    }
}

5
投票

这是基于 Robert 的解决方案,但它适用于 maxLines 和高度限制。

@Preview
@Composable
fun AutoSizePreview1() {
    Box(Modifier.size(200.dp, 300.dp)) {
        AutoSizeText(text = "This is a bunch of text that will fill the box", maxFontSize = 250.sp, maxLines = 2)
    }
}

@Preview
@Composable
fun AutoSizePreview2() {
    Box(Modifier.size(200.dp, 300.dp)) {
        AutoSizeText(text = "This is a bunch of text that will fill the box", maxFontSize = 25.sp)
    }
}

@Preview
@Composable
fun AutoSizePreview3() {
    Box(Modifier.size(200.dp, 300.dp)) {
        AutoSizeText(text = "This is a bunch of text that will fill the box")
    }
}

@Composable
fun AutoSizeText(
    text: String,
    modifier: Modifier = Modifier,
    acceptableError: Dp = 5.dp,
    maxFontSize: TextUnit = TextUnit.Unspecified,
    color: Color = Color.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    contentAlignment: Alignment? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    val alignment: Alignment = contentAlignment ?: when (textAlign) {
        TextAlign.Left -> Alignment.TopStart
        TextAlign.Right -> Alignment.TopEnd
        TextAlign.Center -> Alignment.Center
        TextAlign.Justify -> Alignment.TopCenter
        TextAlign.Start -> Alignment.TopStart
        TextAlign.End -> Alignment.TopEnd
        else -> Alignment.TopStart
    }
    BoxWithConstraints(modifier = modifier, contentAlignment = alignment) {
        var shrunkFontSize = if (maxFontSize.isSpecified) maxFontSize else 100.sp

        val calculateIntrinsics = @Composable {
            val mergedStyle = style.merge(
                TextStyle(
                    color = color,
                    fontSize = shrunkFontSize,
                    fontWeight = fontWeight,
                    textAlign = textAlign,
                    lineHeight = lineHeight,
                    fontFamily = fontFamily,
                    textDecoration = textDecoration,
                    fontStyle = fontStyle,
                    letterSpacing = letterSpacing
                )
            )
            Paragraph(
                text = text,
                style = mergedStyle,
                constraints = Constraints(maxWidth = ceil(LocalDensity.current.run { maxWidth.toPx() }).toInt()),
                density = LocalDensity.current,
                fontFamilyResolver = LocalFontFamilyResolver.current,
                spanStyles = listOf(),
                placeholders = listOf(),
                maxLines = maxLines,
                ellipsis = false
            )
        }

        var intrinsics = calculateIntrinsics()

        val targetWidth = maxWidth - acceptableError / 2f

        check(targetWidth.isFinite || maxFontSize.isSpecified) { "maxFontSize must be specified if the target with isn't finite!" }

        with(LocalDensity.current) {
            // this loop will attempt to quickly find the correct size font by scaling it by the error
            // it only runs if the max font size isn't specified or the font must be smaller
            // minIntrinsicWidth is "The width for text if all soft wrap opportunities were taken."
            if (maxFontSize.isUnspecified || targetWidth < intrinsics.minIntrinsicWidth.toDp())
                while ((targetWidth - intrinsics.minIntrinsicWidth.toDp()).toPx().absoluteValue.toDp() > acceptableError / 2f) {
                    shrunkFontSize *= targetWidth.toPx() / intrinsics.minIntrinsicWidth
                    intrinsics = calculateIntrinsics()
                }
            // checks if the text fits in the bounds and scales it by 90% until it does
            while (intrinsics.didExceedMaxLines || maxHeight < intrinsics.height.toDp() || maxWidth < intrinsics.minIntrinsicWidth.toDp()) {
                shrunkFontSize *= 0.9f
                intrinsics = calculateIntrinsics()
            }
        }

        if (maxFontSize.isSpecified && shrunkFontSize > maxFontSize)
            shrunkFontSize = maxFontSize

        Text(
            text = text,
            color = color,
            fontSize = shrunkFontSize,
            fontStyle = fontStyle,
            fontWeight = fontWeight,
            fontFamily = fontFamily,
            letterSpacing = letterSpacing,
            textDecoration = textDecoration,
            textAlign = textAlign,
            lineHeight = lineHeight,
            onTextLayout = onTextLayout,
            maxLines = maxLines,
            style = style
        )
    }
}

4
投票

我想补充一点,如果你不想从@Brian的回答中看到中间状态,你可以试试这个。

        modifier = Modifier
            .drawWithContent {
                if (calculationFinish) { // replace by your logic 
                    drawContent()
                }
            },

2
投票

尝试 BoxWithConstraints,并学习

SubcomposeLayout
概念

BoxWithConstraints(
    modifier = Modifier
        .fillMaxWidth()
        .weight(5f)
) {
    val size = min(maxWidth * 1.7f, maxHeight)
    val fontSize = size * 0.8f
    Text(
        text = first,
        color = color,
        fontSize = LocalDensity.current.run { fontSize.toSp() },
        modifier = Modifier.fillMaxSize(),
        textAlign = TextAlign.Center,
    )
}

2
投票

更新:这可能在 1.0.1 发布后停止工作....

受@nieto 的回答启发,另一种方法是调整大小而不重新组合,只需在给定入站约束的情况下使用段落块手动测量即可。还可以正确预览作为奖励



@Composable
fun AutoSizeText(
    text: String,
    style: TextStyle,
    modifier: Modifier = Modifier,
    minTextSize: TextUnit = TextUnit.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
) {
    BoxWithConstraints(modifier) {
        var combinedTextStyle = LocalTextStyle.current + style

        while (shouldShrink(text, combinedTextStyle, minTextSize, maxLines)) {
            combinedTextStyle = combinedTextStyle.copy(fontSize = combinedTextStyle.fontSize * .9f)
        }

        Text(
            text = text,
            style = style + TextStyle(fontSize = combinedTextStyle.fontSize),
            maxLines = maxLines,
        )
    }
}

@Composable
private fun BoxWithConstraintsScope.shouldShrink(
    text: String,
    combinedTextStyle: TextStyle,
    minimumTextSize: TextUnit,
    maxLines: Int
): Boolean = if (minimumTextSize == TextUnit.Unspecified || combinedTextStyle.fontSize > minimumTextSize) {
    false
} else {
    val paragraph = Paragraph(
        text = text,
        style = combinedTextStyle,
        width = maxWidth.value,
        maxLines = maxLines,
        density = LocalDensity.current,
        resourceLoader = LocalFontLoader.current,
    )
    paragraph.height > maxHeight.value
}

1
投票

调整了Thad C

的一些解决方案

撰写版本:1.1.0-beta02

预览作品

文本更改时不闪烁,文本更改会得到快速处理(如果文本大小计算在另一个线程上作为协程启动会更好)

@Composable
fun AutoSizeText(
    text: AnnotatedString,
    minTextSizeSp: Float,
    maxTextSizeSp: Float,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    textAlign: TextAlign? = null,
    style: TextStyle = LocalTextStyle.current,
    contentAlignment: Alignment = Alignment.TopStart,
) {
    check(minTextSizeSp > 0) { "Min text size should above zero" }
    check(minTextSizeSp < maxTextSizeSp) { "Min text size should be smaller then max text size" }
    BoxWithConstraints(modifier, contentAlignment = contentAlignment) {
        val textString = text.toString()
        val currentStyle = style.copy(
            color = color,
            fontStyle = fontStyle ?: style.fontStyle,
            fontSize = maxTextSizeSp.sp,
            fontWeight = fontWeight ?: style.fontWeight,
            fontFamily = fontFamily ?: style.fontFamily,
            textAlign = textAlign,
        )
        val fontChecker = createFontChecker(currentStyle, textString)
        val fontSize = remember(textString) {
            fontChecker.findMaxFittingTextSize(minTextSizeSp, maxTextSizeSp)
        }

        Text(
            text = text,
            style = currentStyle + TextStyle(fontSize = fontSize),
            color = color,
            textAlign = textAlign
        )
    }
}

@Composable
private fun BoxWithConstraintsScope.createFontChecker(currentStyle: TextStyle, text: String): FontChecker {
    val density = LocalDensity.current
    return FontChecker(
        density = density,
        resourceLoader = LocalFontLoader.current,
        maxWidthPx = with (density) { maxWidth.toPx() },
        maxHeightPx = with (density) { maxHeight.toPx() },
        currentStyle = currentStyle,
        text = text
    )
}

private class FontChecker(
    private val density: Density,
    private val resourceLoader: Font.ResourceLoader,
    private val maxWidthPx: Float,
    private val maxHeightPx: Float,
    private val currentStyle: TextStyle,
    private val text: String
) {

    fun isFit(fontSizeSp: Float): Boolean {
        val height = Paragraph(
            text = text,
            style = currentStyle + TextStyle(fontSize = fontSizeSp.sp),
            width = maxWidthPx,
            density = density,
            resourceLoader = resourceLoader,
        ).height
        return height <= maxHeightPx
    }

    fun findMaxFittingTextSize(
        minTextSizeSp: Float,
        maxTextSizeSp: Float
    ) = if (!isFit(minTextSizeSp)) {
        minTextSizeSp.sp
    } else if (isFit(maxTextSizeSp)) {
        maxTextSizeSp.sp
    } else {
        var fit = minTextSizeSp
        var unfit = maxTextSizeSp
        while (unfit - fit > 1) {
            val current = fit + (unfit - fit) / 2
            if (isFit(current)) {
                fit = current
            } else {
                unfit = current
            }
        }
        fit.sp
    }
}

1
投票

我发现在@EmbMicro answer maxlines 有时会被忽略。我解决了这个问题,还用

constraints
而不是
with

替换了对 Paragraph 的弃用调用
@Composable
fun AutoSizeText(
    text: String,
    modifier: Modifier = Modifier,
    acceptableError: Dp = 5.dp,
    maxFontSize: TextUnit = TextUnit.Unspecified,
    color: Color = Color.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    BoxWithConstraints(modifier = modifier) {
        var shrunkFontSize = if (maxFontSize.isSpecified) maxFontSize else 100.sp

        val calculateIntrinsics = @Composable {
            val mergedStyle = style.merge(
                TextStyle(
                    color = color,
                    fontSize = shrunkFontSize,
                    fontWeight = fontWeight,
                    textAlign = textAlign,
                    lineHeight = lineHeight,
                    fontFamily = fontFamily,
                    textDecoration = textDecoration,
                    fontStyle = fontStyle,
                    letterSpacing = letterSpacing
                )
            )
            Paragraph(
                text = text,
                style = mergedStyle,
                spanStyles = listOf(),
                placeholders = listOf(),
                maxLines = maxLines,
                ellipsis = false,
                constraints = Constraints(maxWidth = ceil(LocalDensity.current.run { maxWidth.toPx() }).toInt()) ,
                density = LocalDensity.current,
                fontFamilyResolver = LocalFontFamilyResolver.current
            )
        }

        var intrinsics = calculateIntrinsics()

        val targetWidth = maxWidth - acceptableError / 2f

        with(LocalDensity.current) {
            if (maxFontSize.isUnspecified || targetWidth < intrinsics.minIntrinsicWidth.toDp() || intrinsics.didExceedMaxLines) {
                while ((targetWidth - intrinsics.minIntrinsicWidth.toDp()).toPx().absoluteValue.toDp() > acceptableError / 2f) {
                    shrunkFontSize *= targetWidth.toPx() / intrinsics.minIntrinsicWidth
                    intrinsics = calculateIntrinsics()
                }
                while (intrinsics.didExceedMaxLines || maxHeight < intrinsics.height.toDp()) {
                    shrunkFontSize *= 0.9f
                    intrinsics = calculateIntrinsics()
                }
            }
        }

        if (maxFontSize.isSpecified && shrunkFontSize > maxFontSize)
            shrunkFontSize = maxFontSize

        Text(
            text = text,
            color = color,
            fontSize = shrunkFontSize,
            fontStyle = fontStyle,
            fontWeight = fontWeight,
            fontFamily = fontFamily,
            letterSpacing = letterSpacing,
            textDecoration = textDecoration,
            textAlign = textAlign,
            lineHeight = lineHeight,
            onTextLayout = onTextLayout,
            maxLines = maxLines,
            style = style
        )
    }
}

0
投票

这是基于穆罕默德的回答。

你必须找到一种更好的方法来使用框的高度和消息的长度来计算字体大小。

@Composable
fun Greeting() {
    var width by remember { mutableStateOf(0) }
    var height by remember { mutableStateOf(0) }
    val msg = "My really long long long long long text that needs to be resized to the height of this Column"
    Column(modifier = Modifier.height(150.dp).padding(8.dp).background(Color.Blue).onPositioned {
        width = it.size.width
        height = it.size.height
    }) {
        Log.d("mainactivity", "width = $width")
        Log.d("mainactivity", "height = $height")
        Text(
                modifier = Modifier.background(Color.Green).fillMaxHeight(),
                style = TextStyle(fontSize = calculateFontSize(msg, height).sp),
                text = msg
        )
    }
}

fun calculateFontSize(msg: String, height: Int): Int {
    return height / (msg.length / 5)
}

0
投票

我需要使用我自己的字体列表实现可自动调整大小的文本。

所以,这是我基于 Lit Climbing 的回答的实现


0
投票

这是https://gist.github.com/dovahkiin98/cd4e1f639cc4f6392018e327d0db44d8中AutoSizeText的改进版本,优化为:

  1. 无需不必要的迭代即可快速找到合适的文本大小 (二分搜索)
  2. 控制精度
  3. 支持文字对齐
  4. 支撑材料 3

请注意,精度参数定义默认建议文本大小列表中的增量值。此增量值等于 (1/precision).sp。此精度也被强制为 1 和 10.

import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.InternalFoundationTextApi
import androidx.compose.foundation.text.TextDelegate
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.min
import androidx.compose.ui.unit.sp
import kotlin.math.abs
import kotlin.math.ceil

@Composable
fun AutoSizeText(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    suggestedFontSizes: List<TextUnit> = emptyList(),
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current,
    precision: Int = 4,
) {
    AutoSizeText(
        text = AnnotatedString(text),
        modifier = modifier,
        color = color,
        suggestedFontSizes = suggestedFontSizes,
        fontStyle = fontStyle,
        fontWeight = fontWeight,
        fontFamily = fontFamily,
        letterSpacing = letterSpacing,
        textDecoration = textDecoration,
        textAlign = textAlign,
        lineHeight = lineHeight,
        maxLines = maxLines,
        inlineContent = mapOf(),
        onTextLayout = onTextLayout,
        style = style,
        precision = precision
    )
}

@Composable
fun AutoSizeText(
    text: AnnotatedString,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    suggestedFontSizes: List<TextUnit> = emptyList(),
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
    inlineContent: Map<String, InlineTextContent> = mapOf(),
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current,
    precision: Int = 4,
) {
    val coercedPrecision = precision.coerceIn(1, 10)

    BoxWithConstraints(
        modifier = modifier,
        contentAlignment = when (textAlign) {
            TextAlign.Start -> Alignment.CenterStart
            TextAlign.End -> Alignment.CenterEnd
            else -> Alignment.Center
        },
    ) {
        var combinedTextStyle = (LocalTextStyle.current + style).copy(
            fontSize = min(maxWidth, maxHeight).value.sp
        )

        val fontSizes = suggestedFontSizes.ifEmpty {
            val fontSize = ceil(combinedTextStyle.fontSize.value).toInt()
            MutableList(fontSize * coercedPrecision) {
                (fontSize - it / coercedPrecision.toFloat()).sp
            }
        }

        // Dichotomous search
        var currentIndex = 0
        var nextIndex = (fontSizes.count() - 1) / 2

        while (true) {
            val diff = abs(currentIndex - nextIndex)
            if (diff < 2) break // diff < 2 means no change because diff = 1 and diff/2 = 0
            currentIndex = nextIndex
            combinedTextStyle = combinedTextStyle.copy(fontSize = fontSizes[nextIndex])
            nextIndex =
                if (shouldShrink(text, combinedTextStyle, maxLines))
                    nextIndex + diff / 2
                else
                    nextIndex - diff / 2
        }

        var index = minOf(currentIndex, nextIndex)
        combinedTextStyle = combinedTextStyle.copy(fontSize = fontSizes[index])
        while (shouldShrink(text, combinedTextStyle, maxLines)) {
            try {
                combinedTextStyle = combinedTextStyle.copy(fontSize = fontSizes[++index])
            } catch (_: Exception) {
                break
            }
        }

        Text(
            text = text,
            modifier = Modifier,
            color = color,
            fontSize = TextUnit.Unspecified,
            fontStyle = fontStyle,
            fontWeight = fontWeight,
            fontFamily = fontFamily,
            letterSpacing = letterSpacing,
            textDecoration = textDecoration,
            textAlign = textAlign,
            lineHeight = lineHeight,
            overflow = TextOverflow.Clip,
            softWrap = true,
            maxLines = maxLines,
            inlineContent = inlineContent,
            onTextLayout = onTextLayout,
            style = combinedTextStyle,
        )
    }
}

@OptIn(InternalFoundationTextApi::class)
@Composable
private fun BoxWithConstraintsScope.shouldShrink(
    text: AnnotatedString,
    textStyle: TextStyle,
    maxLines: Int,
): Boolean {
    val textDelegate = TextDelegate(
        text = text,
        style = textStyle,
        maxLines = maxLines,
        softWrap = true,
        overflow = TextOverflow.Clip,
        density = LocalDensity.current,
        fontFamilyResolver = LocalFontFamilyResolver.current,
    )

    val textLayoutResult = textDelegate.layout(
        constraints,
        LocalLayoutDirection.current,
    )

    return textLayoutResult.hasVisualOverflow
}
© www.soinside.com 2019 - 2024. All rights reserved.