5

I want to achieve something like the below picture using canvas. How can I achieve something like this?

Enter image description here

Is there some reference that I can look up?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Webster
  • 1,113
  • 2
  • 19
  • 39
  • 1
    Maybe [this](https://dev.to/tkuenneth/drawing-and-painting-in-jetpack-compose-1-2okl) article will help you – ngalashev Aug 24 '22 at 07:26

2 Answers2

16

You can draw it with Path and cubicTo.

path.cubicTo(x1 = x1, y1 = y1, x2 = x2, y2 = y2, x3 = x3, y3 = y3)

For a dashed effect, you should use:

drawPath(
            color = Color.Green,
            path = path,
            style = Stroke(
                width = 3.dp.toPx(),
                pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
            )
        )

With the sample below, I think it will make it easy to understand how the cubic changes by changing the start(x0, y0), end(x3, y3) and control points (x1, y1) and (x2, y2).

Enter image description here

@Composable
fun DrawCubic() {

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {

        val density = LocalDensity.current.density

        val configuration = LocalConfiguration.current
        val screenWidth = configuration.screenWidthDp.dp

        val screenWidthInPx = screenWidth.value * density

        // (x0, y0) is initial coordinate where path is moved with path.moveTo(x0,y0)
        var x0 by remember { mutableStateOf(0f) }
        var y0 by remember { mutableStateOf(0f) }

        /*
        Adds a cubic bezier segment that curves from the current point(x0,y0) to the
        given point (x3, y3), using the control points (x1, y1) and (x2, y2).
     */
        var x1 by remember { mutableStateOf(0f) }
        var y1 by remember { mutableStateOf(screenWidthInPx) }
        var x2 by remember { mutableStateOf(screenWidthInPx/2) }
        var y2 by remember { mutableStateOf(0f) }

        var x3 by remember { mutableStateOf(screenWidthInPx) }
        var y3 by remember { mutableStateOf(screenWidthInPx/2) }

        val path = remember { Path() }
        Canvas(
            modifier = Modifier
                .padding(8.dp)
                .shadow(1.dp)
                .background(Color.White)
                .size(screenWidth, screenWidth/2)
        ) {
            path.reset()
            path.moveTo(x0, y0)
            path.cubicTo(x1 = x1, y1 = y1, x2 = x2, y2 = y2, x3 = x3, y3 = y3)


            drawPath(
                color = Color.Green,
                path = path,
                style = Stroke(
                    width = 3.dp.toPx(),
                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
                )
            )

            // Draw Control Points on screen
            drawPoints(
                listOf(Offset(x1, y1), Offset(x2, y2)),
                color = Color.Green,
                pointMode = PointMode.Points,
                cap = StrokeCap.Round,
                strokeWidth = 40f
            )
        }

        Column(modifier = Modifier.padding(horizontal = 20.dp)) {

            Text(text = "X0: ${x0.roundToInt()}")
            Slider(
                value = x0,
                onValueChange = { x0 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "Y0: ${y0.roundToInt()}")
            Slider(
                value = y0,
                onValueChange = { y0 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "X1: ${x1.roundToInt()}")
            Slider(
                value = x1,
                onValueChange = { x1 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "Y1: ${y1.roundToInt()}")
            Slider(
                value = y1,
                onValueChange = { y1 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "X2: ${x2.roundToInt()}")
            Slider(
                value = x2,
                onValueChange = { x2 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "Y2: ${y2.roundToInt()}")
            Slider(
                value = y2,
                onValueChange = { y2 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "X3: ${x3.roundToInt()}")
            Slider(
                value = x3,
                onValueChange = { x3 = it },
                valueRange = 0f..screenWidthInPx,
            )

            Text(text = "Y3: ${y3.roundToInt()}")
            Slider(
                value = y3,
                onValueChange = { y3 = it },
                valueRange = 0f..screenWidthInPx,
            )
        }
    }
}

More about quads, cubics, paths, Blend modes and Canvas is available in this tutorial.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • I wouldn't have recommended this, sir, for other than academic purposes. Not practical much, you concur? – Richard Onslow Roper Aug 24 '22 at 08:13
  • 1
    You draw the path OP asked with a cubic. And for the arrow path you can add another path at position of (x3, y3). And as i mentioned in answer this answer helps you get how cubic changes when you change control points and start and end points of cubic. – Thracian Aug 24 '22 at 08:18
  • My bad, didn't realise the actual code to draw the cubic is almost the same as the method I illustrated. Rather better, might even say. Got confused by the rest of the codebase so I thought ten lines of code were used to render just the cubic... Lazy reading, sorry. – Richard Onslow Roper Aug 24 '22 at 08:21
  • 1
    Sliders are for demonstrating how control points change a cubic – Thracian Aug 24 '22 at 08:22
  • Yes, I got that, I understand the code. Previously I just glanced at it, looking at the variables made me think 10 vars were required to render the shape, so I thought it'd be better to go for the hard-coded path approach. I have read the entire solution, and the variables were found to be used for enabling dynamic altering of the control points. – Richard Onslow Roper Aug 24 '22 at 08:25
  • 1
    Yes, you are right. OP asks for a reference he/she could look up so i posted not only how you draw a bezier curve but how points you set it with changes curve. Bezier curves can be complicated without a reference, that's why answer is a bit long. Also i draw control points to visualize where they actually are when you change the curve. Have a good day everyone.✌️ – Thracian Aug 24 '22 at 08:27
  • I was about to propose a referencing duel, I guess I can come up with a little under 950 references to put in my answer regarding this, but yeah... good day to you too sir ✌ – Richard Onslow Roper Aug 24 '22 at 08:32
  • thank you for helping me to understand how canvas works Thracian. – Webster Aug 24 '22 at 08:38
  • 1
    awesome! Such a beautiful cubic you have ) – StayCool Oct 07 '22 at 07:10
4

As a general alternative to Thracian's code, which uses the programmatic method to build the path in real-time, for complex shapes/vectors, this is the less painful method:

val pathData = "M 60,60 L 60,0 L 50,10 L 60,0 L 70,10"

Canvas() {
  drawPath(
    path = PathParser.createPathFromPathData(pathData)).asComposePath()
  )
}

This, for example, draws an arrow on the screen. All you need is the pathData for your line/vector. Then it is as simple as assigning that to the pathData variable, as illustrated above.

Also, remember that the PathParser here is the one from androidx.graphics package, not the Compsose Path Parser.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42