Jetpack Compose如何检测用户一笔绘制是否全程在指定矩形区域内
实现Android Compose 描摹绘制区域校验
需求说明
需要实现正方形描摹的一笔画校验逻辑,规则如下:
- 用户必须一次性连贯完成绘制,中途手指抬起即算本笔结束
- 绘制过程中手指移出目标正方形有效区域时,立即重置当前绘制路径,弹出Toast提示绘制失败
- 全程在区域内完成绘制后,弹出Toast提示绘制成功
- 现有代码已实现基础笔迹绘制、白色正方形目标框绘制,目标框通过
drawRect()实现,用户笔迹通过drawPath()实现

现有代码
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 // 路径、当前触点位置、触摸状态定义 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() event.changes .forEachIndexed { index: Int, pointerInputChange: PointerInputChange -> pointerInputChange.consumePositionChange() } motionEvent = actionMove currentPosition = event.changes.first().position } while (event.changes.any { it.pressed }) motionEvent = actionUp } } } Canvas( modifier = drawModifier .padding(20.dp) .size(500.dp) ) { val canvasWidth = size.width val canvasHeight = size.height val line = 1.5f 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) ) } }
实现步骤
1. 修复坐标系错位问题
原代码pointerInput加在全屏大小的modifier上,Canvas又叠加了padding和固定尺寸,会导致手势触点坐标和Canvas内部绘制坐标不一致。需要把pointerInput挪到Canvas的modifier链中,放在padding(20.dp).size(500.dp)之后,保证拿到的触点坐标和绘制坐标系完全对齐。
2. 抽离目标区域边界参数
不要在drawRect方法内写死区域计算逻辑,提前把正方形的边界参数定义成可复用变量,方便手势检测时直接调用。注意Stroke样式的矩形是沿路径居中描边,计算有效区域时要把线宽占的范围算进去。
3. 增加绘制合法性状态
新增状态标记当前这一笔是否始终在有效区域内,同时获取Context用于弹出Toast提示。
4. 全触点流程加区域校验
- 按下事件:第一时间判断触点是否在有效区域内,不在则直接标记本笔无效,弹出失败提示
- 移动事件:每次位置更新都做区域校验,一旦检测到触点越界,立即清空绘制路径、标记本笔无效,弹出失败提示
- 抬起事件:如果本笔全程标记为合法,弹出绘制成功提示,重置状态准备下一次绘制
修改后核心实现代码
class DrawingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyDrawing() } } } @Composable fun MyDrawing() { val context = LocalContext.current val actionIdle = 0 val actionDown = 1 val actionMove = 2 val actionUp = 3 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) } // 标记当前绘制是否合法 var isDrawValid by remember { mutableStateOf(true) } // 统一正方形配置参数,保证绘制和检测逻辑用同一套数值 val squareStrokeWidth = 50.dp.toPx() val canvasSize = 500.dp val squareSizeRatio = 1.5f val baseModifier = Modifier .fillMaxWidth() .fillMaxHeight() .background(canvasColor) .clipToBounds() Canvas( modifier = baseModifier .padding(20.dp) .size(canvasSize) // 修复坐标系偏移:手势检测放在padding、size之后 .pointerInput(Unit) { forEachGesture { awaitPointerEventScope { val canvasWidth = size.width val canvasHeight = size.height val squareSize = canvasWidth / squareSizeRatio val squareTopLeftX = center.x - canvasWidth / 3 val squareTopLeftY = center.y - canvasHeight / 6 // 计算描边正方形的外边界(描边居中,外扩半倍线宽) val halfStroke = squareStrokeWidth / 2 val validLeft = squareTopLeftX - halfStroke val validTop = squareTopLeftY - halfStroke val validRight = squareTopLeftX + squareSize + halfStroke val validBottom = squareTopLeftY + squareSize + halfStroke // 如需严格限制在描边区域绘制,再加内边界判断 val innerLeft = squareTopLeftX + halfStroke val innerTop = squareTopLeftY + halfStroke val innerRight = squareTopLeftX + squareSize - halfStroke val innerBottom = squareTopLeftY + squareSize - halfStroke val down: PointerInputChange = awaitFirstDown().also { motionEvent = actionDown currentPosition = it.position isDrawValid = true // 按下时首次校验 val x = it.position.x val y = it.position.y if (x !in validLeft..validRight || y !in validTop..validBottom) { isDrawValid = false path.reset() Toast.makeText(context, "绘制失败,超出区域", Toast.LENGTH_SHORT).show() } } do { val event: PointerEvent = awaitPointerEvent() event.changes.forEach { pointerInputChange -> pointerInputChange.consumePositionChange() } motionEvent = actionMove currentPosition = event.changes.first().position // 移动过程实时校验 if (isDrawValid) { val x = currentPosition.x val y = currentPosition.y if (x !in validLeft..validRight || y !in validTop..validBottom) { isDrawValid = false path.reset() Toast.makeText(context, "绘制失败,超出区域", Toast.LENGTH_SHORT).show() } } } while (event.changes.any { it.pressed }) motionEvent = actionUp // 抬起时判定结果 if (isDrawValid) { Toast.makeText(context, "绘制成功", Toast.LENGTH_SHORT).show() } // 重置状态准备下一次绘制 path.reset() motionEvent = actionIdle currentPosition = Offset.Unspecified } } } ) { val canvasWidth = size.width val canvasHeight = size.height val squareSize = canvasWidth / squareSizeRatio drawRect( color = Color.White, topLeft = Offset(center.x - canvasWidth / 3, center.y - canvasHeight / 6), size = Size(width = squareSize, squareSize), style = Stroke( width = squareStrokeWidth ), ) // 仅合法状态下记录绘制路径 if (isDrawValid) { 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) } else-> Unit } } drawPath( color = Color.Cyan, path = path, style = Stroke(width = 5.dp.toPx(), join = StrokeJoin.Round) ) } }
补充:如果需要严格校验用户必须沿着白色正方形描边绘制,只要在判断条件里加上「触点不能落在内边界围成的空白区域内」即可,即判断条件改为
(x in validLeft..validRight && y in validTop..validBottom) && (x !in innerLeft..innerRight || y !in innerTop..innerBottom)。
内容的提问来源于stack exchange,提问作者Mohamed Rimshad
相关产品推荐
相关产品推荐

