前言
Jetpack Compose 的拖拽缩放和点击事件。
拖拽和缩放
使用transformable能够比较方便的实现拖拽和缩放
以左上角为中心缩放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| var canvasOffset by remember { mutableStateOf(Offset.Zero) } var canvasScale by remember { mutableFloatStateOf(1f) }
val transformState = rememberTransformableState { zoomChange, panChange, _ -> canvasScale *= zoomChange canvasOffset += panChange } Box( modifier = Modifier .fillMaxSize()
) { Canvas( modifier = Modifier .fillMaxSize() .transformable(state = transformState) .pointerInput(itemList.size) { detectTapGestures( onTap = { val node = itemList.find { node -> val nodePos = (node.position + canvasOffset) * canvasScale val distance = (it - nodePos).getDistance() distance < 100 }
node?.let { nodeClick(it) } } ) } ) { val canvasWidth = size.width val canvasHeight = size.height if (isInit) { canvasOffset = Offset(canvasWidth / 2, canvasHeight / 2) isInit = false }
withTransform({ scale(canvasScale, pivot = Offset.Zero) translate(left = canvasOffset.x, top = canvasOffset.y) }) { } } }
|
以画布中心缩放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| var isInit = true
var canvasOffset by remember { mutableStateOf(Offset.Zero) } var canvasScale by remember { mutableFloatStateOf(1f) }
val transformState = rememberTransformableState { zoomChange, panChange, _ -> canvasScale *= zoomChange canvasOffset += panChange }
var canvasCenter = Offset.Zero
Box( modifier = Modifier .fillMaxSize()
) { Canvas( modifier = Modifier .fillMaxSize() .transformable(state = transformState) .pointerInput(itemList.size) { detectTapGestures( onTap = { val node = itemList.find { node -> val nodePos = (node.position - canvasCenter + canvasOffset) * canvasScale + canvasCenter val distance = (it - nodePos).getDistance() distance < 100 }
node?.let { nodeClick(it) selectId = it.id } } ) } ) { val canvasWidth = size.width val canvasHeight = size.height if (isInit) { canvasCenter = Offset(canvasWidth / 2, canvasHeight / 2) canvasOffset = Offset(canvasWidth / 2, canvasHeight / 2) isInit = false }
withTransform({ scale(canvasScale, pivot = canvasCenter) translate(left = canvasOffset.x, top = canvasOffset.y) }) { } } }
|
这里主要是改了两个地方
scale 中设置了缩放的中心点
1
| scale(canvasScale, pivot = canvasCenter)
|
查找节点的时候也要相应的偏移再计算
1 2 3 4 5 6 7 8
| val node = itemList.find { node -> val nodePos = (node.position - canvasCenter + canvasOffset) * canvasScale + canvasCenter val distance = (it - nodePos).getDistance() distance < 100 }
|
拖拽和缩放方式2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| var canvasOffset by remember { mutableStateOf(Offset.Zero) } var canvasScale by remember { mutableFloatStateOf(1f) }
Box( modifier = Modifier .fillMaxSize()
) { Canvas( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTransformGestures( onGesture = { centroid, pan, zoom, rotation -> if (zoom == 1f) { canvasOffset += pan } else { canvasScale *= zoom canvasOffset += pan } } ) } .pointerInput(itemList.size) { detectTapGestures( onTap = { val node = itemList.find { node -> val nodePos = (node.position + canvasOffset) * canvasScale val distance = (it - nodePos).getDistance() distance < 100 }
node?.let { nodeClick(it) } } ) } ) { val canvasWidth = size.width val canvasHeight = size.height if (isInit) { canvasOffset = Offset(canvasWidth / 2, canvasHeight / 2) isInit = false }
withTransform({ scale(canvasScale, pivot = Offset.Zero) translate(left = canvasOffset.x, top = canvasOffset.y) }) { } } }
|
画布的拖拽移动和点击
我们也可以这样实现拖拽和点击
但是要注意的是
拖拽和点击要放在两个pointerInput中,放在一个中会导致后面的失效。
两个pointerInput的顺序要主要要拖拽的在前,点击事件在后。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| var canvasOffset by remember { mutableStateOf(Offset.Zero) }
Box( modifier = Modifier .fillMaxSize()
) { Canvas( modifier = Modifier .fillMaxSize() .transformable(state = transformState) .pointerInput(Unit) { detectDragGestures( onDrag = { change, dragAmount -> canvasOffset = canvasOffset + dragAmount change.consume() } ) } .pointerInput(itemList.size) { detectTapGestures( onTap = { val node = itemList.find { node -> val nodePos = (node.position + canvasOffset) val distance = (it - nodePos).getDistance() distance < 100 }
node?.let { nodeClick(it) } } ) } ) { val canvasWidth = size.width val canvasHeight = size.height if (isInit) { canvasOffset = Offset(canvasWidth / 2, canvasHeight / 2) isInit = false }
withTransform({ translate(left = canvasOffset.x, top = canvasOffset.y) }) { } } }
|
在 detectTransformGestures 手势监听中,这四个参数分别代表不同的手势信息,具体含义如下:
centroid: Offset
- 它的坐标原点是当前 Canvas 组件的左上角(即该 Composable 在屏幕上的显示区域左上角)
- 例如双指缩放时,它是两个手指之间的中点位置,这也是我们实现 “围绕触摸点缩放” 的核心参考坐标。
pan: Offset
- 表示本次手势事件的平移增量(偏移变化量),单位是像素。
pan.x 是水平方向的移动距离(正值向右,负值向左),pan.y 是垂直方向的移动距离(正值向下,负值向上)。
- 注意这是相对变化量(当前帧与上一帧的差值),而非绝对位置。
zoom: Float
- 表示本次手势事件的缩放比例增量(缩放变化倍数)。
- 例如:当双指张开时,
zoom 会大于 1(如 1.02);当双指捏合时,zoom 会小于 1(如 0.98)。
- 实际缩放比例需要通过累乘计算(
currentScale * zoom)。
rotation: Float
- 表示本次手势事件的旋转角度增量(旋转变化量),单位是弧度(rad)。
- 正值表示顺时针旋转,负值表示逆时针旋转。
- 同样是相对变化量,累计旋转角度需通过累加计算。