Jetpack Compose中实现区域框选 发表于 2025-08-14 | 分类于 android Jetpack Compose中实现区域框选 组件封装123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191import androidx.compose.foundation.Canvasimport androidx.compose.foundation.gestures.detectDragGesturesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.geometry.Offsetimport androidx.compose.ui.geometry.Sizeimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.drawscope.Strokeimport androidx.compose.ui.input.pointer.pointerInputimport androidx.compose.ui.layout.onSizeChangedimport androidx.compose.ui.platform.LocalDensityimport androidx.compose.ui.unit.Dpimport androidx.compose.ui.unit.dpimport com.xhkjedu.zxs_android.common.CommonTheme@Composablefun 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为单位的。 这里我们返回左,上,宽,高都是相对于整体的比例。 使用123456789101112131415161718192021Box( 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) }}