compose 实现AutoSizeText

147 阅读1分钟

使用二分查找法实现Text的字体大小自适应,代码如下:

import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextMeasurer
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.rememberTextMeasurer
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.Constraints
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp

@Composable
fun AutoSizeText(
    text: String,
    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 = TextAlign.Unspecified,
    lineHeight: TextUnit = TextUnit.Unspecified,
    maxLines: Int = 1,
    overflow: TextOverflow = TextOverflow.Ellipsis,
    onTextLayout: ((TextLayoutResult) -> Unit)? = null,
    style: TextStyle = LocalTextStyle.current.copy(
        color = color,
        fontStyle = fontStyle,
        fontWeight = fontWeight,
        fontFamily = fontFamily,
        letterSpacing = letterSpacing,
        textDecoration = textDecoration,
        textAlign = textAlign,
        lineHeight = lineHeight,
    ),
    maxFontSize: TextUnit = 100.sp,
    minFontSize: TextUnit = 10.sp,
) {
    val textMeasurer = rememberTextMeasurer()

    BoxWithConstraints(modifier = modifier) {
        // 1. 获取容器的约束尺寸
        val maxWidthPx = constraints.maxWidth
        val maxHeightPx = constraints.maxHeight

        // 2. 使用 remember 缓存计算结果,只有 text 或约束改变时才重新计算
        val optimizedFontSize = remember(text, maxWidthPx, maxHeightPx) {
            var low = minFontSize.value
            var high = maxFontSize.value
            var res = low

            // 二分查找循环
            while (low <= high) {

                val mid = (low + high) / 2
                val isFit = checkTextFit(
                    textMeasurer = textMeasurer,
                    text = text,
                    fontSize = mid.sp,
                    style = style,
                    maxWidthPx = maxWidthPx,
                    maxHeightPx = maxHeightPx,
                    maxLines = maxLines
                )
                if (isFit) {
                    res = mid
                    low = mid + 0.5f // 尝试更大的字号(步长可调)
                } else {
                    high = mid - 0.5f // 字号太大了,调小一点
                }
            }
            res.sp
        }

        // 3. 最终渲染
        Text(
            text = text,
            color = color,
            fontSize = optimizedFontSize,
            style = style,
            maxLines = maxLines,
            overflow = overflow,
            onTextLayout = onTextLayout
        )
    }
}

/**
 * 核心辅助函数:使用 TextMeasurer 在内存中测量文字是否溢出
 */
private fun checkTextFit(
    textMeasurer: TextMeasurer,
    text: String,
    fontSize: TextUnit,
    style: TextStyle,
    maxWidthPx: Int,
    maxHeightPx: Int,
    maxLines: Int
): Boolean {
    val layoutResult = textMeasurer.measure(
        text = AnnotatedString(text),
        style = style.copy(fontSize = fontSize),
        maxLines = maxLines,
        constraints = Constraints(maxWidth = maxWidthPx, maxHeight = maxHeightPx)
    )
    // 同时检查宽度和高度是否在容器范围内
    return layoutResult.size.width < maxWidthPx && layoutResult.size.height < maxHeightPx
}

以上是完整代码,可直接使用。

简单分析

使用checkTextFit方法计算文字是否超出容器,如果超出则high = mid - 0.5f // 字号太大了,调小一点,否则low = mid + 0.5f // 尝试更大的字号(步长可调),直到low > high,不满足循环条件时,所得到的res就是自适应的字体大小。