Jetpack Compose-Canvas常用事件

前言

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
// 存储画布的整体偏移量,初始为(0,0)
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
// 存储画布的整体偏移量,初始为(0,0)
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
// 存储画布的整体偏移量,初始为(0,0)
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
// 存储画布的整体偏移量,初始为(0,0)
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 手势监听中,这四个参数分别代表不同的手势信息,具体含义如下:

  1. centroid: Offset
    • 它的坐标原点是当前 Canvas 组件的左上角(即该 Composable 在屏幕上的显示区域左上角)
    • 例如双指缩放时,它是两个手指之间的中点位置,这也是我们实现 “围绕触摸点缩放” 的核心参考坐标。
  2. pan: Offset
    • 表示本次手势事件的平移增量(偏移变化量),单位是像素。
    • pan.x 是水平方向的移动距离(正值向右,负值向左),pan.y 是垂直方向的移动距离(正值向下,负值向上)。
    • 注意这是相对变化量(当前帧与上一帧的差值),而非绝对位置。
  3. zoom: Float
    • 表示本次手势事件的缩放比例增量(缩放变化倍数)。
    • 例如:当双指张开时,zoom 会大于 1(如 1.02);当双指捏合时,zoom 会小于 1(如 0.98)。
    • 实际缩放比例需要通过累乘计算(currentScale * zoom)。
  4. rotation: Float
    • 表示本次手势事件的旋转角度增量(旋转变化量),单位是弧度(rad)。
    • 正值表示顺时针旋转,负值表示逆时针旋转。
    • 同样是相对变化量,累计旋转角度需通过累加计算。