Issue
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?
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)
)
}
}
Solution
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
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
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.
Answered By - Thracian
Answer Checked By - Robin (JavaFixing Admin)