You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose文本椭圆框居中失败及自动缩放问题求助

问题解决:椭圆框内文本居中与自适应尺寸调整

问题现象

文本在椭圆框内始终偏移,无法实现水平/垂直居中;同时文本无法根据椭圆框的宽高自动调整字体大小,导致要么超出框体要么尺寸过小。

一、文本偏移问题分析与修复

问题根源

  1. 硬编码偏移值失效:当前用(boundingBox.height() - currentHeight) / 2.5计算垂直偏移,不仅是无依据的魔法值,而且currentHeight是原始字体尺寸下的文本高度,后续字体缩放后该值完全失效,直接导致偏移。
  2. 未考虑文本基线与缩放后尺寸:drawText的topLeft定位依赖缩放后的文本真实尺寸,同时要考虑文本基线的视觉对齐逻辑,仅用原始高度计算偏移必然出现偏差。

修复方案

  • 先基于最终确定的TextStyle测量文本的实际尺寸(缩放后)
  • 计算椭圆框的中心点坐标
  • 用椭圆框中心点减去文本布局的中心点,结合文本基线调整垂直位置,确保视觉上完全居中

二、字体自适应尺寸问题修复

问题根源

  1. 缩放因子逻辑颠倒:原calculateScaledFontSize中用minFontSize * desiredWidth / currentWidth的计算逻辑完全错误,未基于原始字体大小推导缩放比例。
  2. 无适配验证:未验证缩放后的文本是否真的适配椭圆框,仅靠比例计算无法保证文本不超出边界。

修复方案

  • 从最大允许字体大小开始逐步缩小,直到文本尺寸适配椭圆框(预留5%边距避免贴边)
  • 严格限制字体大小在minFontSize和maxFontSize范围内

三、修改后的完整代码

@Composable
internal fun ScreenShotDrawSpeech(
    bubbleDomain: ImmutableList<SpeechBubbleDomain>,
    modifier: Modifier = Modifier,
) {
    val textMeasurer = rememberTextMeasurer()
    val zoom = rememberZoomableState()

    Canvas(
        modifier = modifier
            .fillMaxSize()
            .zoomable(zoom),
        onDraw = {
            bubbleDomain.forEach { prediction ->
                drawSpeechBoundingBox(text = prediction.originalText, boundingBox = prediction.rect, textMeasurer = textMeasurer)
            }
        },
    )
}

private fun DrawScope.drawSpeechBoundingBox(
    boundingBox: Rect,
    text: String,
    textMeasurer: TextMeasurer,
) {
    val ovalLeft = boundingBox.left.toFloat()
    val ovalTop = boundingBox.top.toFloat()
    val ovalWidth = boundingBox.width().toFloat()
    val ovalHeight = boundingBox.height().toFloat()
    val ovalCenter = Offset(ovalLeft + ovalWidth / 2f, ovalTop + ovalHeight / 2f)

    // 基础文本样式(不含字体大小)
    val baseStyle = TextStyle(
        fontFamily = FontFamily(Font(R.font.manga_master_bb)),
        color = Color.Black,
        background = Color.White,
        textAlign = TextAlign.Center,
        fontWeight = FontWeight.Bold,
    )
    val minFontSizePx = 12.sp.toPx()
    val maxFontSizePx = 14.sp.toPx()

    // 计算适配椭圆框的字体大小
    val adaptiveFontSizePx = calculateAdaptiveFontSize(
        text = text.uppercase(),
        targetWidth = ovalWidth,
        targetHeight = ovalHeight,
        baseStyle = baseStyle,
        textMeasurer = textMeasurer,
        minFontSizePx = minFontSizePx,
        maxFontSizePx = maxFontSizePx,
    )

    // 最终文本样式
    val finalStyle = baseStyle.copy(fontSize = adaptiveFontSizePx.sp)
    // 测量缩放后的文本布局
    val textLayoutResult = textMeasurer.measure(
        text = text.uppercase(),
        style = finalStyle,
        constraints = Constraints(maxWidth = ovalWidth.toInt(), maxHeight = ovalHeight.toInt())
    )
    val textWidth = textLayoutResult.size.width.toFloat()
    val textBaseline = textLayoutResult.firstBaseline

    // 计算文本居中的topLeft坐标:椭圆中心 - 文本中心,调整基线偏移保证垂直居中
    val textLeft = ovalCenter.x - textWidth / 2f
    val textTop = ovalCenter.y - textBaseline + (ovalHeight - textLayoutResult.size.height) / 2f

    // 绘制椭圆框
    drawOval(
        color = Color.White,
        topLeft = Offset(ovalLeft, ovalTop),
        size = Size(ovalWidth, ovalHeight),
    )
    drawOval(
        color = Color.Black,
        style = Stroke(width = 2f),
        topLeft = Offset(ovalLeft, ovalTop),
        size = Size(ovalWidth, ovalHeight),
    )

    // 绘制居中文本
    drawText(
        textMeasurer = textMeasurer,
        style = finalStyle,
        text = text.uppercase(),
        topLeft = Offset(textLeft, textTop),
        constraints = Constraints(maxWidth = ovalWidth.toInt(), maxHeight = ovalHeight.toInt())
    )
}

private fun calculateAdaptiveFontSize(
    text: String,
    targetWidth: Float,
    targetHeight: Float,
    baseStyle: TextStyle,
    textMeasurer: TextMeasurer,
    minFontSizePx: Float,
    maxFontSizePx: Float,
): Float {
    var fontSize = maxFontSizePx
    // 预留5%边距,避免文本贴椭圆框边缘
    val safeTargetWidth = targetWidth * 0.95f
    val safeTargetHeight = targetHeight * 0.95f

    while (fontSize >= minFontSizePx) {
        val layoutResult = textMeasurer.measure(
            text = text,
            style = baseStyle.copy(fontSize = fontSize.sp),
            constraints = Constraints(maxWidth = safeTargetWidth.toInt(), maxHeight = safeTargetHeight.toInt())
        )
        val textWidth = layoutResult.size.width.toFloat()
        val textHeight = layoutResult.size.height.toFloat()

        if (textWidth <= safeTargetWidth && textHeight <= safeTargetHeight) {
            return fontSize
        }
        // 每次缩小1px,可根据需求调整步长
        fontSize -= 1f
    }
    // 最小字体仍超出时,返回最小允许值
    return minFontSizePx
}

关键修改点说明

  • 文本居中:通过椭圆框中心点与文本布局的基线、尺寸计算定位,彻底消除硬编码偏移,保证视觉居中。
  • 字体自适应:从最大字体逐步缩小验证,确保文本在椭圆框内适配,同时严格限制字体范围。
  • 布局测量时机:确定最终字体后再测量文本布局,保证使用的是缩放后的真实尺寸,避免计算偏差。

内容的提问来源于stack exchange,提问作者OliverDamon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 12:45:10