Search code examples
androidandroid-jetpack-composeandroid-jetpack-compose-canvasandroid-jetpack-compose-gesture

Is it possible to attach event handlers like onClick ,drag to the content being drawn within a Canvas Composable or by using the drawBehind Modifier?


I'm working on a visual graph/tree editor where each node is represented as a circle. I need to draw edges between these nodes, and in some instances, these edges shouldn't be straight lines. I want to enable users to interactively manipulate the edges by allowing them to bend the edges click or dragging a particular edge, as well as move and rotate the edge's cost or weight (represented as text using drawText) when the user taps or clicks on a specific edge. cost.How can I achieve this functionality?

I tried using transfromGesture and dragGesture but I found that these gestures are only applicable to composables.


Solution

  • You can't assign a Modifier to what you draw inside Canvas, which is Spacer with Modifier.drawBehind, or other drawings inside draw Modifiers.

    But you can store positions and radius, angle and other visual properties of your nodes inside a data class and draw them using the values inside these classes. When user interacts with any node you update these properties and canvas reading these properties draw your node tree correctly.

    If shapes you wish to detect if they are touched are circle it's easy to detect based on distance to center, but you can use Path too if you want to.

    I post a sample with circles drawn randomly on screen since i don't have data structure to draw nodes but it should give an idea how to implement it.

    enter image description here

    @Preview
    @Composable
    private fun Test() {
    
        val drawList = remember {
            mutableStateListOf<DrawProperties>()
        }
    
        var touchIndex by remember {
            mutableStateOf(-1)
        }
    
        LaunchedEffect(Unit) {
            repeat(5) {
                val properties = DrawProperties(
                    center = Offset(
                        Random.nextInt(100, 1000).toFloat(),
                        Random.nextInt(100, 1500).toFloat()
                    )
                )
    
                drawList.add(properties)
            }
        }
    
        Canvas(
            modifier = Modifier.fillMaxSize()
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDragStart = { offset ->
                            touchIndex = -1
                            drawList.forEachIndexed { index, drawProperties ->
                                val isTouched =
                                    isTouched(drawProperties.center, offset, drawProperties.radius)
    
                                if (isTouched) {
                                    touchIndex = index
                                }
                            }
                        },
                        onDrag = { change, dragAmount: Offset ->
                            val item = drawList.getOrNull(touchIndex)
                            item?.let { drawItem ->
                                drawList[touchIndex] = drawItem.copy(
                                    center = drawItem.center.plus(dragAmount),
                                    color = Color.Green
                                )
                            }
                        },
                        onDragEnd = {
                            val item = drawList.getOrNull(touchIndex)
                            item?.let { drawItem ->
                                drawList[touchIndex] = drawItem.copy(
                                    color = Color.Red
                                )
                            }
                        }
                    )
                }
        ) {
            drawList.forEachIndexed { index, drawProperties ->
    
                if (touchIndex != index) {
                    drawCircle(
                        color = drawProperties.color,
                        center = drawProperties.center,
                        radius = drawProperties.radius
                    )
                }
            }
    
            if (touchIndex > -1) {
                drawList.getOrNull(touchIndex)?.let { drawProperties ->
                    drawCircle(
                        color = drawProperties.color,
                        center = drawProperties.center,
                        radius = drawProperties.radius
                    )
                }
            }
        }
    
    }
    
    private fun isTouched(center: Offset, touchPosition: Offset, radius: Float): Boolean {
        return center.minus(touchPosition).getDistanceSquared() < radius * radius
    }
    
    data class DrawProperties(
        val center: Offset,
        val radius: Float = 80f,
        val color: Color = Color.Red
    )