1

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.

Thracian
  • 43,021
  • 16
  • 133
  • 222
MajinKenn
  • 47
  • 5

1 Answers1

1

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.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • 1
    Guess this is one of those reasons why this is part of experimentalApi, so its not guaranteed to run properly. Regardless, thank for you for accepted answer because it works perfectly if I use this version of the function. – MajinKenn Aug 25 '23 at 22:53
  • Yes, it will very likely to be fixed in the future. But for now you can use this overload. – Thracian Aug 26 '23 at 03:51