Search code examples
androidkotlinandroid-jetpack-composeandroid-canvas

Draw straight lines on canvas


I am trying to draw straight lines using canvas and the detectDragGestures method, but so far I have only achieved chaotic behavior without any fixations. I am seeking a solution to ensure that my lines are always straight.

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CanvasAction() {
    var centerOffset by remember {
        mutableStateOf(Offset.Zero)
    }

    var points by remember {
        mutableStateOf<List<Point>>(listOf())
    }

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(key1 = Unit) {
                detectDragGestures(
                    onDragStart = { offset ->
                        points = points + Point(offset = offset, isStartedPosition = true)
                    },
                    onDrag = { change, offset ->
                        change.historical.forEach {
                            centerOffset = it.position
                        }

                        points = points + change.historical.map {
                            Point(offset = it.position, isStartedPosition = false)
                        }
                    },
                )
            },
    ) {
        drawCircle(
            color = Color.Blue,
            radius = 7.dp.toPx(),
            center = centerOffset,
        )

        val path = Path()

        points.forEach { point ->
            if (point.isStartedPosition) {
                path.moveTo(point.offset.x, point.offset.y)
            } else {
                path.lineTo(point.offset.x, point.offset.y)
            }
        }

        drawPath(
            path = path,
            color = Color.Blue,
            style = Stroke(width = 2.dp.toPx()),
        )
    }
}

my example


Solution

  • The problem is that you save each single point during the drag operation.

    If you instead only remember the starting point during onDragStart and calculate the current point during onDrag while ignoring any intermediate points, you can draw a path between those two points and always have a straight line.

    If you want to repeat this for multiple drag gestures while keeping the previous lines you need to use onDragEnd to save the current line.

    It could look like this:

    @Composable
    fun CanvasAction() {
        var centerOffset by remember {
            mutableStateOf<Offset?>(null)
        }
    
        var finishedLines by remember {
            mutableStateOf<List<Rect>>(emptyList())
        }
    
        var currentLine by remember {
            mutableStateOf<Rect?>(null)
        }
    
        Canvas(
            modifier = Modifier
                .fillMaxSize()
                .pointerInput(key1 = Unit) {
                    detectDragGestures(
                        onDragStart = { offset ->
                            centerOffset = offset
                            currentLine = Rect(offset, offset)
                        },
                        onDrag = { _, dragAmount ->
                            centerOffset = centerOffset?.plus(dragAmount)
                            currentLine = currentLine?.let {
                                it.copy(
                                    right = it.right + dragAmount.x,
                                    bottom = it.bottom + dragAmount.y,
                                )
                            }
                        },
                        onDragEnd = {
                            centerOffset = null
                            currentLine?.let { finishedLines += it }
                            currentLine = null
                        },
                        onDragCancel = {
                            centerOffset = null
                            currentLine = null
                        },
                    )
                },
        ) {
            centerOffset?.let {
                drawCircle(
                    color = Color.Blue,
                    radius = 7.dp.toPx(),
                    center = it,
                )
            }
    
            val linesToDraw = currentLine?.let { finishedLines + it } ?: finishedLines
            val path = Path()
    
            linesToDraw.forEach {
                path.moveTo(it.left, it.top)
                path.lineTo(it.right, it.bottom)
            }
    
            drawPath(
                path = path,
                color = Color.Blue,
                style = Stroke(width = 2.dp.toPx()),
            )
        }
    }
    

    To represent a line I have used an androidx.compose.ui.geometry.Rect that has two offset coordinates that I use to save the starting point and the ending point of the line; the Point class isn't used any more. finishedLines is a list of all lines that were already drawn, where currentLine is the line that, well, currently is drawn.

    The other changes mostly regard null handling when there currently is nothing being drawn.