Compose Canvas 中添加手势控制
在 Compose Canvas 中集成手势控制是创建交互式绘图应用的关键。以下是结合 Canvas 和手势控制的全面指南:
核心实现模式
1. 基础手势集成
@Composable
fun GestureControlledCanvas() {// 状态管理var position by remember { mutableStateOf(Offset.Zero) }var scale by remember { mutableStateOf(1f) }var rotation by remember { mutableStateOf(0f) }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {// 检测拖动手势detectDragGestures { change, dragAmount ->position += dragAmount}// 检测缩放和旋转手势detectTransformGestures { centroid, pan, zoom, rotationChange ->position += panscale *= zoomrotation += rotationChange}// 检测点击手势detectTapGestures(onDoubleTap = { // 双击重置position = Offset.Zeroscale = 1frotation = 0f})}) {// 应用变换withTransform({translate(position.x, position.y)scale(scale, scale, pivot = center)rotate(rotation, pivot = center)}) {// 绘制内容drawRect(Color.Blue, topLeft = Offset(-50f, -50f), size = Size(100f, 100f))drawCircle(Color.Red, center = Offset(0f, 0f), radius = 50f)}}
}
2. 复杂交互实现
拖拽绘制对象
@Composable
fun DraggableShapesCanvas() {// 可拖拽的形状列表val shapes = remember {mutableStateListOf(ShapeData(Offset(100f, 100f), Color.Blue, 80f),ShapeData(Offset(300f, 300f), Color.Red, 60f),ShapeData(Offset(500f, 200f), Color.Green, 100f))}// 当前拖拽的形状var draggedShape by remember { mutableStateOf<ShapeData?>(null) }var dragOffset by remember { mutableStateOf(Offset.Zero) }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {detectDragGestures(onDragStart = { offset ->// 查找点击位置下的形状draggedShape = shapes.find { shape ->val distance = (shape.position - offset).getDistance()distance < shape.radius}draggedShape?.let {dragOffset = offset - it.position}},onDrag = { change, dragAmount ->draggedShape?.let {it.position = change.position - dragOffset}},onDragEnd = {draggedShape = null})}) {// 绘制所有形状shapes.forEach { shape ->drawCircle(color = shape.color,center = shape.position,radius = shape.radius)}}
}data class ShapeData(var position: Offset,val color: Color,val radius: Float
)
缩放和平移视图
@Composable
fun ZoomableCanvas() {var translation by remember { mutableStateOf(Offset.Zero) }var scale by remember { mutableStateOf(1f) }var rotation by remember { mutableStateOf(0f) }// 跟踪两点触摸距离(用于计算缩放)var initialDistance by remember { mutableStateOf(0f) }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {awaitPointerEventScope {while (true) {val event = awaitPointerEvent()// 单点触摸 - 拖动if (event.changes.size == 1) {val change = event.changes[0]if (change.pressed) {translation += change.positionChange()}}// 多点触摸 - 缩放和旋转if (event.changes.size >= 2) {val change1 = event.changes[0]val change2 = event.changes[1]val currentDistance = (change1.position - change2.position).getDistance()if (initialDistance == 0f) {initialDistance = currentDistance}// 计算缩放比例scale *= currentDistance / initialDistance// 计算旋转角度val initialAngle = atan2(change1.position.y - change2.position.y,change1.position.x - change2.position.x)val currentAngle = atan2(change1.position.y - change2.position.y,change1.position.x - change2.position.x)rotation += (currentAngle - initialAngle).toDegrees()initialDistance = currentDistance} else {initialDistance = 0f}}}}) {withTransform({translate(translation.x, translation.y)scale(scale, scale, pivot = center)rotate(rotation, pivot = center)}) {// 绘制复杂内容drawComplexContent()}}
}fun DrawScope.drawComplexContent() {// 绘制网格背景for (x in 0..size.width.toInt() step 50) {drawLine(Color.LightGray,start = Offset(x.toFloat(), 0f),end = Offset(x.toFloat(), size.height),strokeWidth = 0.5f)}for (y in 0..size.height.toInt() step 50) {drawLine(Color.LightGray,start = Offset(0f, y.toFloat()),end = Offset(size.width, y.toFloat()),strokeWidth = 0.5f)}// 绘制中心标记drawCircle(Color.Red, center = center, radius = 10f)// 绘制变换指示器drawText("Scale: ${"%.2f".format(scale)}",Offset(20f, 40f),color = Color.Black,fontSize = 20.sp)drawText("Rotation: ${rotation.toInt()}°",Offset(20f, 70f),color = Color.Black,fontSize = 20.sp)
}// 扩展函数:弧度转角度
fun Float.toDegrees() = (this * 180 / Math.PI).toFloat()
3. 手势控制绘图应用
@Composable
fun DrawingApp() {// 绘图状态val paths = remember { mutableStateListOf<Path>() }val currentPath = remember { Path() }val currentColor = remember { mutableStateOf(Color.Black) }val brushSize = remember { mutableStateOf(8f) }// 手势状态var isDrawing by remember { mutableStateOf(false) }var startPosition by remember { mutableStateOf(Offset.Zero) }Box(modifier = Modifier.fillMaxSize()) {Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {detectDragGestures(onDragStart = { offset ->isDrawing = truestartPosition = offsetcurrentPath.moveTo(offset.x, offset.y)},onDrag = { change, _ ->if (isDrawing) {currentPath.lineTo(change.position.x, change.position.y)}},onDragEnd = {isDrawing = falsepaths.add(currentPath)currentPath.reset()})}) {// 绘制所有路径paths.forEach { path ->drawPath(path = path,color = currentColor.value,style = Stroke(width = brushSize.value, cap = StrokeCap.Round))}// 绘制当前路径drawPath(path = currentPath,color = currentColor.value,style = Stroke(width = brushSize.value, cap = StrokeCap.Round))}// 控制面板DrawingControls(currentColor = currentColor,brushSize = brushSize,onClear = { paths.clear() })}
}@Composable
fun DrawingControls(currentColor: MutableState<Color>,brushSize: MutableState<Float>,onClear: () -> Unit
) {Row(modifier = Modifier.fillMaxWidth().padding(16.dp).background(Color.White.copy(alpha = 0.8f)).padding(8.dp),horizontalArrangement = Arrangement.SpaceBetween) {// 颜色选择Row {listOf(Color.Black, Color.Red, Color.Blue, Color.Green).forEach { color ->Box(modifier = Modifier.size(36.dp).background(color).border(width = if (currentColor.value == color) 2.dp else 0.dp,color = Color.DarkGray).clickable { currentColor.value = color })}}// 笔刷大小Slider(value = brushSize.value,onValueChange = { brushSize.value = it },valueRange = 2f..30f,modifier = Modifier.width(200.dp))// 清除按钮IconButton(onClick = onClear) {Icon(Icons.Default.Delete, contentDescription = "Clear")}}
}
4. 手势控制图表交互
@Composable
func InteractiveChart() {val dataPoints = remember { listOf(50f, 120f, 80f, 200f, 150f, 180f, 100f) }var viewport by remember { mutableStateOf(Rect(0f, 0f, dataPoints.size.toFloat(), 250f))}var hoverIndex by remember { mutableStateOf(-1) }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {detectDragGestures(onDrag = { change, dragAmount ->// 平移视图viewport = viewport.translate(-dragAmount.x * 0.1f, dragAmount.y * 0.1f)})detectTapGestures { offset ->// 查找点击的数据点val index = ((offset.x - paddingPx) / (size.width - 2 * paddingPx) * dataPoints.size).toInt().coerceIn(0, dataPoints.size - 1)// 显示/隐藏提示hoverIndex = if (hoverIndex == index) -1 else index}}) {val paddingPx = 50fval chartWidth = size.width - 2 * paddingPxval chartHeight = size.height - 2 * paddingPx// 绘制坐标轴drawLine(Color.Black,start = Offset(paddingPx, paddingPx),end = Offset(paddingPx, size.height - paddingPx),strokeWidth = 2f)drawLine(Color.Black,start = Offset(paddingPx, size.height - paddingPx),end = Offset(size.width - paddingPx, size.height - paddingPx),strokeWidth = 2f)// 绘制数据点dataPoints.forEachIndexed { index, value ->val x = paddingPx + (index.toFloat() / (dataPoints.size - 1)) * chartWidthval y = size.height - paddingPx - (value / viewport.height) * chartHeight// 绘制数据点drawCircle(color = if (index == hoverIndex) Color.Red else Color.Blue,center = Offset(x, y),radius = 8f)// 绘制连接线if (index > 0) {val prevX = paddingPx + ((index - 1).toFloat() / (dataPoints.size - 1)) * chartWidthval prevY = size.height - paddingPx - (dataPoints[index - 1] / viewport.height) * chartHeightdrawLine(Color.Blue,start = Offset(prevX, prevY),end = Offset(x, y),strokeWidth = 3f)}// 显示悬停信息if (index == hoverIndex) {drawCircle(Color.Red, center = Offset(x, y), radius = 12f, style = Stroke(2f))// 绘制信息框drawRect(color = Color.White,topLeft = Offset(x - 60f, y - 80f),size = Size(120f, 60f),style = Fill)drawRect(color = Color.Black,topLeft = Offset(x - 60f, y - 80f),size = Size(120f, 60f),style = Stroke(1f))drawText("Value: ${value.toInt()}",Offset(x - 55f, y - 55f),color = Color.Black,fontSize = 16.sp)drawText("Index: $index",Offset(x - 55f, y - 30f),color = Color.Black,fontSize = 16.sp)}}}
}
高级技巧与最佳实践
1. 手势冲突解决方案
Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {coroutineScope {// 优先级较高的手势launch { detectTapGestures(onDoubleTap = { /* 处理双击 */ }) }// 优先级较低的手势launch { detectDragGestures { /* 处理拖动 */ }}// 最低优先级的手势launch { detectTransformGestures { /* 处理缩放旋转 */ }}}}
) {// 绘制内容
}
2. 性能优化
@Composable
fun OptimizedGestureCanvas() {// 使用 remember 缓存路径val paths = remember { mutableStateListOf<Path>() }val currentPath = remember { Path() }// 使用 derivedStateOf 减少不必要的重绘val pathCount by remember { derivedStateOf { paths.size } }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {detectDragGestures(onDragStart = { offset ->currentPath.moveTo(offset.x, offset.y)},onDrag = { change, _ ->currentPath.lineTo(change.position.x, change.position.y)},onDragEnd = {paths.add(currentPath)currentPath.reset()})}.drawWithCache {// 缓存静态路径val staticPath = Path()paths.forEach { path -> staticPath.addPath(path) }onDraw {// 绘制静态路径(高效)drawPath(staticPath, Color.Black, style = Stroke(8f))// 绘制当前路径(动态)drawPath(currentPath, Color.Blue, style = Stroke(8f))}})
}
3. 复杂手势识别
@Composable
fun ShapeRecognitionCanvas() {val path = remember { Path() }var recognizedShape by remember { mutableStateOf<ShapeType?>(null) }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {detectDragGestures(onDragStart = { offset -> path.reset()path.moveTo(offset.x, offset.y)recognizedShape = null},onDrag = { change, _ -> path.lineTo(change.position.x, change.position.y)},onDragEnd = {recognizedShape = recognizeShape(path)})}) {// 绘制路径drawPath(path, Color.Black, style = Stroke(4f))// 显示识别结果recognizedShape?.let {drawText("Detected: ${it.name}",Offset(50f, 50f),color = Color.Red,fontSize = 24.sp)}}
}enum class ShapeType { CIRCLE, RECTANGLE, TRIANGLE, LINE, UNKNOWN }fun recognizeShape(path: Path): ShapeType {// 简化路径分析逻辑val bounds = path.getBounds()val width = bounds.widthval height = bounds.heightif (width < 5f || height < 5f) return ShapeType.UNKNOWN// 计算路径长度与包围盒周长比例val pathLength = path.calculateLength()val boundingPerimeter = 2 * (width + height)val ratio = pathLength / boundingPerimeterreturn when {ratio > 0.9 && ratio < 1.1 -> ShapeType.RECTANGLEratio > 0.7 && ratio < 0.9 -> ShapeType.CIRCLEpath.segmentCount == 3 -> ShapeType.TRIANGLEpath.segmentCount == 2 -> ShapeType.LINEelse -> ShapeType.UNKNOWN}
}// 扩展函数:计算路径长度
fun Path.calculateLength(): Float {var length = 0fval points = PathMeasure(this, false).apply {length = length}return length
}// 扩展函数:获取路径段数
val Path.segmentCount: Intget() {var count = 0PathMeasure(this, false).apply {do {count++} while (nextContour())}return count}
实用调试工具
@Composable
fun DebugGestureCanvas() {var lastEvent by remember { mutableStateOf("") }var touchPoints by remember { mutableStateOf(listOf<Offset>()) }Canvas(modifier = Modifier.fillMaxSize().pointerInput(Unit) {awaitPointerEventScope {while (true) {val event = awaitPointerEvent()lastEvent = when (event.type) {PointerEventType.Press -> "Press: ${event.changes.size} points"PointerEventType.Release -> "Release"PointerEventType.Move -> "Move"else -> "Unknown"}touchPoints = event.changes.map { it.position }}}}) {// 绘制触摸点touchPoints.forEach { point ->drawCircle(Color.Red, center = point, radius = 20f)drawText("(${"%.0f".format(point.x)}, ${"%.0f".format(point.y)})",point + Offset(0f, -30f),color = Color.Black)}// 显示事件信息drawText(lastEvent,Offset(20f, 50f),color = Color.Blue,fontSize = 20.sp)// 显示触摸点计数drawText("Points: ${touchPoints.size}",Offset(20f, 80f),color = Color.Blue,fontSize = 20.sp)}
}
总结关键点
-
状态管理:使用
mutableStateOf
跟踪手势交互状态 -
手势检测:
-
使用
detectDragGestures
处理拖拽 -
使用
detectTransformGestures
处理缩放旋转 -
使用
detectTapGestures
处理点击
-
-
绘制优化:
-
使用
drawWithCache
缓存静态内容 -
使用
derivedStateOf
减少不必要的重绘
-
-
坐标变换:
-
使用
withTransform
应用平移、缩放、旋转 -
在变换后绘制交互内容
-
-
多点触控:
-
通过
awaitPointerEvent().changes
访问所有触摸点 -
实现复杂的手势识别逻辑
-
通过结合 Compose Canvas 的强大绘图能力和手势控制,你可以创建丰富的交互式图形应用,从简单的绘图板到复杂的数据可视化工具。