androidkotlinandroid-jetpack-composeandroid-canvas

Automatically scale text size into bounding box area


I'm trying to center a text within a rectangle according to its height and width, but it comes out off-center. Additionally, I need the text to adjust according to the size of this rectangle. However, I'm not finding very good solutions.

Would anyone know where the error might be?

Result:

enter image description here

My code for help:

@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 currentWidth = textMeasurer.measure(text).size.width
    val currentHeight = textMeasurer.measure(text).size.height
    val desiredWidth = boundingBox.width()
    val desiredHeight = boundingBox.height()

    val centerTextY = (boundingBox.height() - currentHeight) / 2.5

    val font = calculateScaledFontSize(
        currentWidth = currentWidth,
        currentHeight = currentHeight,
        desiredWidth = desiredWidth,
        desiredHeight = desiredHeight,
        minFontSize = 12.sp.toPx(),
        maxFontSize = 14.sp.value,
        text = text,
    )

    val style = TextStyle(
        fontSize = font.sp,
        fontFamily = FontFamily(Font(R.font.manga_master_bb)),
        color = Color.Black,
        background = Color.White,
        textAlign = TextAlign.Center,
        fontWeight = FontWeight.Bold,
    )

    drawOval(
        color = Color.White,
        topLeft = Offset(boundingBox.left.toFloat(), boundingBox.top.toFloat()),
        size = Size(boundingBox.width().toFloat(), boundingBox.height().toFloat()),
    )
    drawOval(
        color = Color.Black,
        style = Stroke(width = 2f),
        topLeft = Offset(boundingBox.left.toFloat(), boundingBox.top.toFloat()),
        size = Size(boundingBox.width().toFloat(), boundingBox.height().toFloat()),
    )
    drawText(
        textMeasurer = textMeasurer,
        style = style,
        size = Size(boundingBox.width().toFloat(), boundingBox.height().toFloat()),
        topLeft = Offset(
            boundingBox.left.toFloat(),
            boundingBox.top.toFloat() + centerTextY.toFloat(),
        ),
        text = text.uppercase(),
    )
}

@Suppress("LongParameterList")
private fun calculateScaledFontSize(
    currentWidth: Int,
    currentHeight: Int,
    desiredWidth: Int,
    desiredHeight: Int,
    minFontSize: Float,
    maxFontSize: Float,
    text: String,
): Float {
    val widthScaleFactor = minFontSize * desiredWidth / currentWidth
    val heightScaleFactor = minFontSize * desiredHeight / currentHeight

    return if (min(widthScaleFactor, heightScaleFactor) >= maxFontSize) {
        if (text.length <= 10) {
            min(widthScaleFactor, heightScaleFactor) / 3
        } else {
            min(widthScaleFactor, heightScaleFactor) / 2
        }
    } else {
        min(widthScaleFactor, heightScaleFactor)
    }
}

Solution

  • You're calculating the Y-coordinate for centering the text using a fixed value 2.5 which can cause the text to be off-center.

    @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 currentSize = textMeasurer.measure(text).size
        val desiredSize = Size(boundingBox.width(), boundingBox.height())
    
        // Calculate the font size to fit both width and height
        val font = calculateScaledFontSize(currentSize, desiredSize)
    
        val style = TextStyle(
            fontSize = font.sp,
            fontFamily = FontFamily(Font(R.font.manga_master_bb)),
            color = Color.Black,
            background = Color.White,
            textAlign = TextAlign.Center,
            fontWeight = FontWeight.Bold,
        )
    
        // Draw bounding box
        drawOval(
            color = Color.White,
            topLeft = Offset(boundingBox.left.toFloat(), boundingBox.top.toFloat()),
            size = Size(boundingBox.width().toFloat(), boundingBox.height().toFloat()),
        )
        drawOval(
            color = Color.Black,
            style = Stroke(width = 2f),
            topLeft = Offset(boundingBox.left.toFloat(), boundingBox.top.toFloat()),
            size = Size(boundingBox.width().toFloat(), boundingBox.height().toFloat()),
        )
    
        // Calculate the position to center the text within the bounding box
        val centerTextX = boundingBox.left + (boundingBox.width() - currentSize.width * font) / 2
        val centerTextY = boundingBox.top + (boundingBox.height() - currentSize.height * font) / 2
    
        // Draw text centered within the bounding box
        drawIntoCanvas {
            it.nativeCanvas.drawText(
                text.uppercase(),
                centerTextX,
                centerTextY,
                Paint().apply {
                    this.textSize = font.sp
                    this.color = Color.Black.toArgb()
                    this.typeface = Typeface.create(Font(R.font.manga_master_bb).toFamily().first(), Typeface.BOLD)
                }
            )
        }
    

    //Updated font scaling function

    private fun calculateScaledFontSize(
            currentSize: Size,
            desiredSize: Size,
        ): Float {
            val widthScaleFactor = desiredSize.width / currentSize.width
            val heightScaleFactor = desiredSize.height / currentSize.height
            // Choose the smaller scale factor to ensure the text fits within the bounding box
            val scaleFactor = min(widthScaleFactor, heightScaleFactor)
            // Cap the scale factor to avoid excessive font enlargement
                return min(1f, scaleFactor) // Adjust the cap value as needed
        }
    

    I have updated your code with sample I was having from in code, please give it a try.