Jetpack Compose中实现区域框选

组件封装

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.xhkjedu.zxs_android.common.CommonTheme


@Composable
fun ResizableBox(
defaultWidth: Dp = 520.dp,
defaultHeight: Dp = 360.dp,
changeAction: (left: Float, top: Float, width: Float, height: Float) -> Unit
) {
// 圆点的半径
val DOT_RADIUS = 20f
val density = LocalDensity.current

// 初始框选区域的左上角和右下角坐标(相对于父容器)
var topLeft by remember { mutableStateOf(Offset(0f, 0f)) }
var bottomRight by remember { mutableStateOf(Offset(0f, 0f)) }
// 当前被拖动的圆点(null 表示没有拖动任何圆点)

var draggingDot by remember { mutableStateOf<Int?>(null) }

var containerHeightPx: Float by remember { mutableStateOf(0f) }
var containerWidthPx: Float by remember { mutableStateOf(0f) }

val borderWidth: Dp = 3.dp

// 计算四个角圆点的坐标
// 0: 左上角, 1: 右上角, 2: 左下角, 3: 右下角
var dotPositions = listOf(
topLeft, // 左上角
Offset(bottomRight.x, topLeft.y), // 右上角
Offset(topLeft.x, bottomRight.y), // 左下角
bottomRight // 右下角
)

Canvas(
modifier = Modifier
.fillMaxSize()
.onSizeChanged { size ->
containerHeightPx = size.height.toFloat()
containerWidthPx = size.width.toFloat()
val containerWidth = with(density) { size.width.toDp() }
val containerHeight = with(density) { size.height.toDp() }
val left = ((containerWidth - defaultWidth) / 2)
val top = (containerHeight - defaultHeight) / 2
val leftPx = with(density) { left.toPx() }
val topPx = with(density) { top.toPx() }
val widthPx = with(density) { defaultWidth.toPx() }
val heightPx = with(density) { defaultHeight.toPx() }
topLeft = Offset(leftPx, topPx)
bottomRight =
Offset(leftPx + widthPx, topPx + heightPx)

changeAction(
topLeft.x / containerWidthPx,
topLeft.y / containerHeightPx,
(bottomRight.x - topLeft.x) / containerWidthPx,
(bottomRight.y - topLeft.y) / containerHeightPx
)
}
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { startOffset ->
dotPositions = listOf(
topLeft, // 左上角
Offset(bottomRight.x, topLeft.y), // 右上角
Offset(topLeft.x, bottomRight.y), // 左下角
bottomRight // 右下角
)
// 判断点击位置是否在某个圆点内
draggingDot = dotPositions.indexOfFirst { dotOffset ->
isPointInCircle(startOffset, dotOffset, DOT_RADIUS + 20)
}.takeIf { it != -1 }

},
onDrag = { change, dragAmount ->
change.consume()
draggingDot?.let { dotIndex ->
// 根据拖动的圆点更新对应坐标
when (dotIndex) {
0 -> { // 左上角
val newX = (topLeft.x + dragAmount.x).coerceIn(
0f,
bottomRight.x - 2 * DOT_RADIUS
)
val newY = (topLeft.y + dragAmount.y).coerceIn(
0f,
bottomRight.y - 2 * DOT_RADIUS
)
topLeft = Offset(newX, newY)
}

1 -> { // 右上角
val newX = (bottomRight.x + dragAmount.x).coerceIn(
topLeft.x + 2 * DOT_RADIUS,
size.width.toFloat()
)
val newY = (topLeft.y + dragAmount.y).coerceIn(
0f,
bottomRight.y - 2 * DOT_RADIUS
)
topLeft = topLeft.copy(y = newY)
bottomRight = bottomRight.copy(x = newX)
}

2 -> { // 左下角
val newX = (topLeft.x + dragAmount.x).coerceIn(
0f,
bottomRight.x - 2 * DOT_RADIUS
)
val newY = (bottomRight.y + dragAmount.y).coerceIn(
topLeft.y + 2 * DOT_RADIUS,
size.height.toFloat()
)
topLeft = topLeft.copy(x = newX)
bottomRight = bottomRight.copy(y = newY)
}

3 -> { // 右下角
val newX = (bottomRight.x + dragAmount.x).coerceIn(
topLeft.x + 2 * DOT_RADIUS,
size.width.toFloat()
)
val newY = (bottomRight.y + dragAmount.y).coerceIn(
topLeft.y + 2 * DOT_RADIUS,
size.height.toFloat()
)
bottomRight = Offset(newX, newY)
}
}
}
},
onDragEnd = {
draggingDot = null
changeAction(
topLeft.x / containerWidthPx,
topLeft.y / containerHeightPx,
(bottomRight.x - topLeft.x) / containerWidthPx,
(bottomRight.y - topLeft.y) / containerHeightPx
)
}
)
}
) {
// 绘制框选区域的矩形边框
drawRect(
color = Color.White,
topLeft = topLeft,
size = Size(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y),
style = Stroke(width = borderWidth.toPx())
)

// 绘制四个角的圆点
dotPositions.forEach { dotOffset ->
drawCircle(
color = CommonTheme.ColorWhite,
radius = DOT_RADIUS + borderWidth.toPx(),
center = dotOffset
)
drawCircle(
color = CommonTheme.ColorBlue,
radius = DOT_RADIUS,
center = dotOffset
)
}
}
}

// 判断点是否在圆内
private fun isPointInCircle(point: Offset, circleCenter: Offset, circleRadius: Float): Boolean {
val dx = point.x - circleCenter.x
val dy = point.y - circleCenter.y
return Math.abs(dx) < circleRadius && Math.abs(dy) < circleRadius
}

注意

  • 绘制的时候的单位都是PX,我们要做DP和PX之间的转换
  • 发生变化回调返回的也是PX,是因为我们图片也是PX为单位的。

这里我们返回左,上,宽,高都是相对于整体的比例。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = leftRightSpace, end = leftRightSpace)
) {
Image(
painter = BitmapPainter(
it.asImageBitmap()
),
modifier = Modifier
.fillMaxSize()
.background(Color(0xaa000000)),
contentScale = ContentScale.FillBounds,
contentDescription = "显示Bitmap图片"
)
ResizableBox() {
left: Float, top: Float, width: Float, height: Float ->
Log.i(TAG, "ResizableBox width: " + width)
Log.i(TAG, "ResizableBox height: " + height)
}
}