Graph Editor part 1

Centering the shapes under the finger

When the user taps the Canvas and we add a new shape, the shape's upper-left corner appears under their finger. This feels a little off, like when we drew the triangle at the center of the Canvas.

Finger is at upper-left corner

To fix this, we simply need to subtract the half-shape offset from the finger-tap location when creating the shape

show in full file app/src/main/java/com/androidbyexample/graph/Graph.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Graph(
    // ...
) {
    with(LocalDensity.current) {
        // ...
        Scaffold(
            // ...
        ) { innerPadding ->
            Canvas(modifier = modifier
                .padding(innerPadding)
                .fillMaxSize()
                .pointerInput(true) {
                    detectTapGestures { finger ->
                        when(val selectedTool = selectedToolRef) {
                            is ShapeType -> {
//                              onAddShapeRef(Shape(selectedTool, finger))
                                onAddShapeRef(
                                    Shape(
                                        shapeType = selectedTool,
                                        offset = finger - halfShapeOffset
                                    ))
                            }
                            Line -> TODO()
                            // ...
                        }
                    }
                }
            ) {
                // ...
            }
        }
    }
}

The shape is now centered on the finger-tap location.

Finger is at upper-left corner


All code changes

CHANGED: app/src/main/java/com/androidbyexample/graph/Graph.kt
package com.androidbyexample.graph

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Graph(
    shapes: List<Shape>,
    onAddShape: (Shape) -> Unit,
    selectedTool: ToolType,
    onSelectedToolChanged: (ToolType) -> Unit,
    modifier: Modifier = Modifier,
    shapeSizeDp: Dp = 48.dp,
    shapeOutlineWidthDp: Dp = 3.dp,
    shapeOutlineColor: Color = Color.Black,
    triangleColor: Color = Color.Red,
    circleColor: Color = Color.Blue,
    squareColor: Color = Color.Green,
    selectedToolColor: Color = MaterialTheme.colorScheme.secondary,
) {
    with(LocalDensity.current) {
        val shapeSizePx = shapeSizeDp.toPx()
        val halfShapeSizePx = shapeSizePx/2
        val shapeOutlineWidthPx = shapeOutlineWidthDp.toPx()
        val shapeSize = Size(shapeSizePx, shapeSizePx)
        val halfShapeOffset = Offset(halfShapeSizePx, halfShapeSizePx)

        val trianglePath = Path().apply {
            moveTo(shapeSizePx/2, 0f)
            lineTo(shapeSizePx, shapeSizePx)
            lineTo(0f, shapeSizePx)
            close()
        }

        val shapeOutlineStroke = remember(shapeOutlineWidthPx) {
            Stroke(shapeOutlineWidthPx)
        }

        fun DrawScope.drawTriangle(
            offset: Offset,
        ) {
            translate(offset.x, offset.y) {
                drawPath(
                    path = trianglePath,
                    color = triangleColor,
                    style = Fill,
                )
                drawPath(
                    path = trianglePath,
                    color = shapeOutlineColor,
                    style = shapeOutlineStroke,
                )
            }
        }
        fun DrawScope.drawSquare(
            offset: Offset,
        ) {
            translate(offset.x, offset.y) {
                drawRect(
                    topLeft = Offset.Zero,
                    size = shapeSize,
                    color = squareColor,
                    style = Fill,
                )
                drawRect(
                    topLeft = Offset.Zero,
                    size = shapeSize,
                    color = shapeOutlineColor,
                    style = shapeOutlineStroke,
                )
            }
        }
        fun DrawScope.drawCircle(
            offset: Offset,
        ) {
            translate(offset.x, offset.y) {
                drawCircle(
                    center = halfShapeOffset,
                    radius = halfShapeSizePx,
                    color = circleColor,
                    style = Fill,
                )
                drawCircle(
                    center = halfShapeOffset,
                    radius = halfShapeSizePx,
                    color = shapeOutlineColor,
                    style = shapeOutlineStroke,
                )
            }
        }

        val selectedToolRef by rememberUpdatedState(selectedTool)
        val onAddShapeRef by rememberUpdatedState(onAddShape)

        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text(stringResource(R.string.graph)) },
                    actions = {
                        ToolButton(
                            draw = { drawSquare(it) },
                            shapeSizeDp = shapeSizeDp,
                            tool = Square,
                            selectedTool = selectedTool,
                            selectionColor = selectedToolColor,
                            onSelectTool = onSelectedToolChanged,
                        )
                        ToolButton(
                            draw = { drawCircle(it) },
                            shapeSizeDp = shapeSizeDp,
                            tool = Circle,
                            selectedTool = selectedTool,
                            selectionColor = selectedToolColor,
                            onSelectTool = onSelectedToolChanged,
                        )
                        ToolButton(
                            draw = { drawTriangle(it) },
                            shapeSizeDp = shapeSizeDp,
                            tool = Triangle,
                            selectedTool = selectedTool,
                            selectionColor = selectedToolColor,
                            onSelectTool = onSelectedToolChanged,
                        )
                    }
                )
            },
            modifier = modifier,
        ) { innerPadding ->
            Canvas(modifier = modifier
                .padding(innerPadding)
                .fillMaxSize()
                .pointerInput(true) {
                    detectTapGestures { finger ->
                        when(val selectedTool = selectedToolRef) {
is ShapeType -> { // onAddShapeRef(Shape(selectedTool, finger)) onAddShapeRef( Shape( shapeType = selectedTool, offset = finger - halfShapeOffset )) }
Line -> TODO() Select -> TODO() } } } ) { shapes.forEach { shape -> when(shape.shapeType) { Square -> drawSquare(shape.offset) Circle -> drawCircle(shape.offset) Triangle -> drawTriangle(shape.offset) } } } } } }