Search code examples
androidkotlinandroid-jetpack-composedrawtextandroid-jetpack-compose-canvas

Android Compose - Why is drawText in Canvas being cut off at bottom but not at top?


enter image description here

I'm relatively new to Android Compose and as a project, I'm trying to design a bar graph. Gathering various examples, I'm now trying to design the axes in a Canvas composable with a DrawScope. Once I've drawn the lines, my next task was to create the labels for the y-axis on the right hand side of the Canvas. But as shown in the picture, trying to align the text to the proper dividing lines on the y-axis, I've hit a snag where the final drawText is off the canvas at the bottom. Yet, it can draw off on the top of the canvas. I'm not sure why, but it probably is a flaw with the way I've written the composable below.

    @OptIn(ExperimentalTextApi::class)
    @Composable
    fun BarGraphAxes(modifier: Modifier,
                     verticalLines: Int,
                     horizontalLines : Int) {
        val barAxisColor = colorResource(id = R.color.grey)
        val textColor = colorResource(id = R.color.textprimary)
        val configuration = LocalConfiguration.current

        Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top) {
            Box(modifier = Modifier
                .width(configuration.screenWidthDp.dp)
                .height(300.dp)
                .padding(start = 30.dp, top = 20.dp)
            ) {
                val textMeasurer = rememberTextMeasurer()
                Canvas(modifier = Modifier.fillMaxSize()) {
                    val barWidthPx =
                        1.dp.toPx() //calculate in DP before converting to Px is the Compose preferred way to draw
                    val yAxisBottomExtraOffset = 20.dp.toPx()
                    val dashPathEffect =
                        PathEffect.dashPathEffect(
                            intervals = floatArrayOf(10f, 10f),
                            phase = 0f
                        )
                    val myWidth = (size.width.toDp() - 50.dp).toPx()
                    val topPaddingForAxes = 15

                    //draw X-Axis
                    drawLine(
                        color = barAxisColor,
                        start = Offset(0f, size.height),
                        end = Offset(myWidth, size.height),
                        strokeWidth = barWidthPx,
                    )

                    //draw Y-Axis right hand
                    drawLine(
                        color = barAxisColor,
                        start = Offset(myWidth, 0f),
                        end = Offset(myWidth, size.height + yAxisBottomExtraOffset),
                        strokeWidth = barWidthPx,
                        pathEffect = dashPathEffect
                    )

                    //draw Y-Axis left hand
                    drawLine(
                        color = barAxisColor,
                        start = Offset(0f, 0f),
                        end = Offset(0f, size.height + yAxisBottomExtraOffset),
                        strokeWidth = barWidthPx,
                        pathEffect = dashPathEffect
                    )

                    //draw X-Axis top
                    drawLine(
                        color = barAxisColor,
                        start = Offset(0f, 0f),
                        end = Offset(myWidth, 0f),
                        strokeWidth = barWidthPx,
                    )

                    val horizontalSize = size.height / (horizontalLines + 1)
                    val verticalSize = myWidth / (verticalLines + 1) //distance between the axes

                    //Note, this is not where the final drawText will be at, just experimenting with static positioning.
                    drawText(textMeasurer, text = "Label", style = TextStyle(fontSize = 12.sp, color = textColor), overflow = TextOverflow.Ellipsis, topLeft = Offset(myWidth, 0f - 20f))
                    drawText(textMeasurer, text = "Label", style = TextStyle(fontSize = 12.sp, color = textColor), overflow = TextOverflow.Ellipsis, topLeft = Offset(myWidth, horizontalSize - 20f))
                    drawText(textMeasurer, text = "Label", style = TextStyle(fontSize = 12.sp, color = textColor), overflow = TextOverflow.Ellipsis, topLeft = Offset(myWidth, (horizontalSize * 2) - 20f))
                    drawText(textMeasurer, text = "Label", style = TextStyle(fontSize = 12.sp, color = textColor), overflow = TextOverflow.Ellipsis, topLeft = Offset(myWidth, (horizontalSize * 3) - 20f))


                    //draw the dividing vertical axes
                    repeat(verticalLines) { i ->
                        val startXCoordinate = verticalSize * (i + 1) //start at 0
                        drawLine(
                            color = barAxisColor,
                            start = Offset(startXCoordinate, 0f),
                            end = Offset(
                                startXCoordinate,
                                size.height + yAxisBottomExtraOffset
                            ),
                            strokeWidth = barWidthPx,
                            pathEffect = dashPathEffect
                        )
                    }

                    //draw the dividing horizontal axes
                    repeat(horizontalLines) { i ->
                        val startYCoordinate = horizontalSize * (i + 1)
                        drawLine(
                            color = barAxisColor,
                            start = Offset(0f, startYCoordinate),
                            end = Offset(myWidth, startYCoordinate),
                            strokeWidth = barWidthPx
                        )
                    }
                }
            }
        }
    }

If anyone can point me in the right direction on why drawText goes off at the bottom but not at the top, I'd appreciate it.


Solution

  • There might be a bug with overload function that takes TextMeasurer as param. If you use

    val textMeasurer = rememberTextMeasurer()
    val result = remember {
        textMeasurer.measure(
            text="Label",
            style = TextStyle(fontSize = 12.sp, color = textColor),
            overflow = TextOverflow.Ellipsis
        )
    }
    

    And draw with

    drawText(
        textLayoutResult = result,
        topLeft = Offset(myWidth, (horizontalSize * 3) - 20f)
    )
    

    It works fine. You should be able to draw out of Canvas anything unless Canvas or any of its parents are clipped.