Jetpack compose Drawing over shapes
Asked Answered
B

1

3

I have this interesting problem in a project, where user should be able to draw the same shape over the defined shape, i have achieved this so far, but i want to check if he/she drawn over the shape correctly in ONE GO. if the finger goes outside the sqaure the current drawing should reset and put a toast message as unsucssesfull else says succesfull, How do i check if the drawing is on the Square?

image

The white square is drawn with drawRect() Method and drawing over it is by the user itself, achieved by Drawpath(). code is given below

class DrawingActivity : ComponentActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyDrawing()
        }
    }
}
@Composable
fun MyDrawing() {
    val actionIdle = 0
    val actionDown = 1
    val actionMove = 2
    val actionUp = 3
    //Path, current touch position and touch states
    val path = remember { Path() }
    var motionEvent by remember { mutableStateOf(actionIdle) }
    var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
    val canvasColor: Color by remember { mutableStateOf(Color.LightGray) }
    val drawModifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight()
        .background(canvasColor)
        .clipToBounds()
        .pointerInput(Unit) {
            forEachGesture {
                awaitPointerEventScope {

                    val down: PointerInputChange = awaitFirstDown().also {
                        motionEvent = actionDown
                        currentPosition = it.position
                    }
                    do {
                        val event: PointerEvent = awaitPointerEvent()

                        var eventChanges =
                            "DOWN changedToDown: ${down.changedToDown()} changedUp: ${down.changedToUp()}\n"
                        event.changes
                            .forEachIndexed { index: Int, pointerInputChange: PointerInputChange ->
                                eventChanges += "Index: $index, id: ${pointerInputChange.id}, " +
                                        "changedUp: ${pointerInputChange.changedToUp()}" +
                                        "pos: ${pointerInputChange.position}\n"

                                pointerInputChange.consumePositionChange()
                            }

                        //gestureText = "EVENT changes size ${event.changes.size}\n" + eventChanges

                        //gestureColor = Color.Green
                        motionEvent = actionMove
                        currentPosition = event.changes.first().position
                    } while (event.changes.any { it.pressed })

                    motionEvent = actionUp
                    //canvasColor = Color.LightGray

                    //gestureText += "UP changedToDown: ${down.changedToDown()} " + "changedUp: ${down.changedToUp()}\n"
                }
            }

        }


    Canvas(
        modifier = drawModifier
            .padding(20.dp)
            .size(500.dp)
    ) {
        val canvasWidth = size.width
        val canvasHeight = size.height
        val line = 1.5
        val squareSize = canvasWidth/line

        drawRect(
            color = Color.White,
            topLeft = Offset(center.x - canvasWidth / 3, center.y - canvasHeight / 6),
            size = Size(width = squareSize.toFloat(), squareSize.toFloat()),
            style = Stroke(
                width = 50.dp.toPx()
            ),
        )

        when(motionEvent){
            actionDown->{
                path.moveTo(currentPosition.x,currentPosition.y)
            }
            actionMove->{
                if (currentPosition!= Offset.Unspecified){
                    path.lineTo(currentPosition.x,currentPosition.y)

                }
            }
            actionUp->{
                path.lineTo(currentPosition.x,currentPosition.y)
                motionEvent = actionIdle


            }
            else-> Unit
        }

       drawPath(
           color = Color.Cyan,
           path = path,
           style = Stroke(width = 5.dp.toPx(), join = StrokeJoin.Round)
       )

    }

}
Berg answered 13/7, 2022 at 15:12 Comment(0)
S
3

You can either get Rect of your Path using path.getBounds(), and compare it with user's current touch position. Here i add a sample for this. I don't check if it got error or finish in one touch you can implement that. This one checks in which bound we are currently, if we are in green rect we are in correct bounds

@Composable
private fun CanvasShapeSample() {

    // This is motion state. Initially or when touch is completed state is at MotionEvent.Idle
    // When touch is initiated state changes to MotionEvent.Down, when pointer is moved MotionEvent.Move,
    // after removing pointer we go to MotionEvent.Up to conclude drawing and then to MotionEvent.Idle
    // to not have undesired behavior when this composable recomposes. Leaving state at MotionEvent.Up
    // causes incorrect drawing.
    var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
    // This is our motion event we get from touch motion
    var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
    // This is previous motion event before next touch is saved into this current position
    var previousPosition by remember { mutableStateOf(Offset.Unspecified) }

    val innerPath = remember { Path() }
    val outerPath = remember { Path() }

    // Path is what is used for drawing line on Canvas
    val path = remember { Path() }


    var isError by remember { mutableStateOf(false) }


    val drawModifier = Modifier
        .fillMaxSize()
        .background(Color.LightGray)
        .pointerMotionEvents(
            onDown = { pointerInputChange: PointerInputChange ->
                currentPosition = pointerInputChange.position
                motionEvent = MotionEvent.Down
                pointerInputChange.consume()
            },
            onMove = { pointerInputChange: PointerInputChange ->
                currentPosition = pointerInputChange.position
                motionEvent = MotionEvent.Move
                pointerInputChange.consume()
            },
            onUp = { pointerInputChange: PointerInputChange ->
                motionEvent = MotionEvent.Up
                pointerInputChange.consume()
            },
            delayAfterDownInMillis = 25L
        )

    Canvas(modifier = drawModifier) {

        val canvasWidth = size.width
        val canvasHeight = size.height

        val outerShapeWidth = canvasWidth * .8f
        val innerShapeWidth = canvasWidth * .6f

        if (innerPath.isEmpty) {

            innerPath.addRect(
                Rect(
                    offset = Offset(
                        (canvasWidth - innerShapeWidth) / 2,
                        (canvasHeight - innerShapeWidth) / 2
                    ),
                    size = Size(innerShapeWidth, innerShapeWidth)
                )
            )
        }


        if (outerPath.isEmpty) {
            outerPath.addRect(
                Rect(
                    offset = Offset(
                        (canvasWidth - outerShapeWidth) / 2,
                        (canvasHeight - outerShapeWidth) / 2
                    ),
                    size = Size(outerShapeWidth, outerShapeWidth)
                )
            )
        }


        when (motionEvent) {
            MotionEvent.Down -> {
                path.moveTo(currentPosition.x, currentPosition.y)
                previousPosition = currentPosition
                isError = !isInBound(innerPath = innerPath, outerPath = outerPath, currentPosition)
            }

            MotionEvent.Move -> {
                path.quadraticBezierTo(
                    previousPosition.x,
                    previousPosition.y,
                    (previousPosition.x + currentPosition.x) / 2,
                    (previousPosition.y + currentPosition.y) / 2

                )
                previousPosition = currentPosition
                isError = !isInBound(innerPath = innerPath, outerPath = outerPath, currentPosition)
            }

            MotionEvent.Up -> {
                path.lineTo(currentPosition.x, currentPosition.y)
                currentPosition = Offset.Unspecified
                previousPosition = currentPosition
                motionEvent = MotionEvent.Idle
            }

            else -> Unit
        }

        drawPath(color = Color.Green, path = outerPath)
        drawPath(color = Color.Yellow, path = innerPath)


        drawPath(
            color = Color.Red,
            path = path,
            style = Stroke(width = 4.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round)
        )

        drawCircle(
            color = if (isError) Color.Red else Color.Green,
            center = Offset(100f, 100f),
            radius = 50f
        )
    }
}

private fun isInBound(innerPath: Path, outerPath: Path, position: Offset): Boolean {
    val innerRect = innerPath.getBounds()
    val outerRect = outerPath.getBounds()

    return !innerRect.contains(position) && outerRect.contains(position)
}

Result

enter image description here

If your shape is complex what you can do is getting path segments and check if they are out of bounds of your complex shape

        val segments: Iterable<PathSegment> = path.asAndroidPath().flatten()

pathSegment has start and end PointF values. If user moves pointer fast it might not create enough pathSegments but it would proabably be an edge case.

This tutorial has section about path segments checking it and sample above would give an idea. But this will probably very difficult for complex shapes which might require you to ask question for algorithm for detection if a position is inside a Path

https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials#6-1-5-canvas-path-segments

enter image description here

I see that you use my code for drawing over canvas i mentioned here. I simplified it you can check these gestures to see how simple it's now. You don't need all that code.

Sternmost answered 13/7, 2022 at 16:6 Comment(12)
Hi, Yes, I have used your code :), This is a bit confusing since this is my very first drawing/compose project. If you could some code here based on my code. That would be great.Berg
okay i will try to add some tomorrow. You want user to draw inside white square right?Sternmost
Yes, wonderful, I managed to get val pathOfDrawing = path.getBounds(). but struggling to get that with the shape.Berg
Actually without bounds, you can just use the currentPosition from down and move events. Just check it with Rectangle. Rect has a rect.contains(Offset) method. on each gesture compare it with the bounds if your shape is always be a Rect.Sternmost
it won't be always rect apparentlyBerg
I mean have 2 rectangles, ok? First one, is inner one that you don't want your offset to be in and the white one that you want it to be in.Sternmost
Let us continue this discussion in chat.Sternmost
hello, may i know which version of dependency you are using for the code you provided, lot of the things are unresolvable, such as Down,Up,size,position, fillMaxSizeBerg
pointerMotionEvents and MotionEvent are from this library github.com/SmartToolFactory/Compose-Extended-Gestures. com.github.SmartToolFactory:Compose-Extended-Gestures:2.0.0.Sternmost
Works great, was wondering if you have a medium blog or youtube channel, excited to do some experiments with your github repo :)Berg
Unfortunately i don't have a blog or youtube channel. You can follow github if you want to. Tutorial covers gestures, custom layouts, canvas and many other things. Your question is pretty interesting and difficult btw. My answer is just a tool. Finding an algorithm to detect if drawing is in valid path not going back, not intersecting will be hard. One of my coworker had worked in a similar project for indoor to keep user on path and it was quite hard. You need to search algorithms to find a good solution and if you do apply it other areas eitherSternmost
Maybe you can create a third path between outer and inner path and check closest distance your current position to path if it's out of desired threshold then show an error when your shapes get more complex. This is something you should ask with algorithm tag i guessSternmost

© 2022 - 2024 — McMap. All rights reserved.