效果图
分析思路
首先需要一个可以拖动的的圆,当拖动圆的时候,在原位置画一个新的圆,随着拖动的距离变大或缩小,更改原位置圆的半径,当超过一定的距离,不再绘制原位置的圆。还有个需要考虑的问题是,原位置的圆的半径变化并不直接是手指拖动的距离,否则产生的变化太快。绘制了两个圆后,剩下就是要考虑绘制两个圆之间的连接路径效果,这个需要用到二阶贝塞尔曲线的知识点。
二阶贝塞尔曲线
简单解释:给定一个起始点(红色)和一个终点(蓝色),再加一个控制点(绿色),可以得到一条曲线。
所以重点是如何算出起始点,控制点和终点的坐标。
计算各点的坐标
A点所在的圆就是“分析思路”中原位置的圆,E点的圆为手势拖动的圆。
E点坐标(offsetX, offsetY),minDragDistance为AE长度,计算涉及到一点点小学或初中的数学基础。
这里唯一需要确定的是正负方向,坐标系中右方和下方为正方向。拿G点举例子:G点为(负,正),此时offsetY和offsetX都为正,算出来的sin和cos也为正,sin和cos与半径相乘后任然为正,由于G点符号为(负,正),所以G点的坐标可确定为circleStartX = 0 - circleRaidus * sin; circleStartY = 0 + circleRadius * cos。
其他点的符号同理可确定。这里的控制点取的是两个圆心连线的中点。
绘制贝塞尔路径
确定点的坐标后,后面就很简单了,仅仅是参数的传递。
path.reset()
path.moveTo(circleStartX, circleStartY)
path.quadraticBezierTo(controlX, controlY, bubbleEndX, bubbleEndY)
path.lineTo(bubbleStartX, bubbleStartY)
path.quadraticBezierTo(controlX, controlY, circleEndX, circleEndY)
drawPath(path, Color.Red)
依次调用moveTo,quadraticBezierTo,lineTo,quadraticBezierTo
附上完整代码供大家学习
data class DragStatus(
//是否正在拖动
val onDrag: Boolean = false,
//是否超出最小的距离
val onDragExceedDistance: Boolean = false,
//超出距离,气泡消失
val onDragComplete: Boolean = false
)
@OptIn(ExperimentalTextApi::class)
@Composable
fun MessageBubble() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var controlX by remember { mutableStateOf(0f) }
var controlY by remember { mutableStateOf(0f) }
var sin by remember { mutableStateOf(0f) }
var cos by remember { mutableStateOf(0f) }
var circleStartX by remember { mutableStateOf(0f) }
var circleStartY by remember { mutableStateOf(0f) }
var bubbleEndX by remember { mutableStateOf(0f) }
var bubbleEndY by remember { mutableStateOf(0f) }
var bubbleStartX by remember { mutableStateOf(0f) }
var bubbleStartY by remember { mutableStateOf(0f) }
var circleEndX by remember { mutableStateOf(0f) }
var circleEndY by remember { mutableStateOf(0f) }
var dragStatus by remember {
mutableStateOf(DragStatus())
}
var circleRadius by remember { mutableStateOf(MainActivity.RADIUS) }
val bubbleRadius by remember { mutableStateOf(MainActivity.RADIUS) }
var minDragDistance by remember { mutableStateOf(0f) }
var previousDistance by remember { mutableStateOf(0f) }
var textWidth by remember { mutableStateOf(0f) }
var textHeight by remember { mutableStateOf(0f) }
val textLayoutResult = rememberTextMeasurer().measure(AnnotatedString("66"))
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Button(modifier = Modifier.padding(top = 100.dp), onClick = {
circleRadius = MainActivity.RADIUS
previousDistance = 0f
minDragDistance = 0f
offsetX = 0f
offsetY = 0f
dragStatus = dragStatus.copy(
onDrag = false,
onDragExceedDistance = false,
onDragComplete = false
)
}) {
Text(text = "reset")
}
Canvas(modifier = Modifier, onDraw = {
if (dragStatus.onDrag && !dragStatus.onDragExceedDistance) {
//原点的圆
drawCircle(Color.Red, circleRadius)
}
})
val path = Path()
Canvas(
modifier = Modifier
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
offsetX += dragAmount.x
offsetY += dragAmount.y
minDragDistance = sqrt(
(offsetX.toDouble().pow(2.0) + offsetY.toDouble()
.pow(2.0)).toFloat()
)
//这里为什么除以8?除以几都可以看你心情,主要是不让原点的圆因为短距离的拖动而缩小的太快
if (minDragDistance > previousDistance) {
circleRadius -= ((minDragDistance - previousDistance) / 8f)
} else if (minDragDistance < previousDistance) {
circleRadius += ((previousDistance - minDragDistance) / 8f)
}
previousDistance = minDragDistance
dragStatus = dragStatus.copy(onDrag = true)
if (minDragDistance > MainActivity.MIN_DRAG_DISTANCE) {
dragStatus = dragStatus.copy(onDragExceedDistance = true)
}
},
onDragEnd = {
dragStatus = dragStatus.copy(onDrag = false)
if (minDragDistance < MainActivity.MIN_DRAG_DISTANCE) {
//拖动距离小于设置的距离,让可拖动的小圆回到原点
offsetX = 0f
offsetY = 0f
minDragDistance = 0f
circleRadius = MainActivity.RADIUS
dragStatus = dragStatus.copy(
onDrag = false,
onDragExceedDistance = false
)
} else {
dragStatus = dragStatus.copy(
onDragExceedDistance = true,
onDragComplete = true
)
}
}
)
}, onDraw = {
//如果正在拖动小圆,并且没有超过一定距离,才画连接的路径
if (dragStatus.onDrag && !dragStatus.onDragExceedDistance) {
controlX = (offsetX + 0) / 2
controlY = (offsetY + 0) / 2
sin = offsetY / minDragDistance
cos = offsetX / minDragDistance
circleStartX = 0f - circleRadius * sin
circleStartY = 0f + circleRadius * cos
bubbleEndX = offsetX - bubbleRadius * sin
bubbleEndY = offsetY + bubbleRadius * cos
bubbleStartX = offsetX + bubbleRadius * sin
bubbleStartY = offsetY - bubbleRadius * cos
circleEndX = 0f + circleRadius * sin
circleEndY = 0f - circleRadius * cos
println(
"---------------------------\n" +
"minDragDistance: $minDragDistance\n" +
"offsetX: $offsetX, offsetY: $offsetY\n" +
"($circleStartX, $circleStartY)\n" +
"($controlX, $controlY, $bubbleEndX, $bubbleEndY)\n" +
"($bubbleStartX, $bubbleStartY)\n" +
"($controlX, $controlX, $circleEndX, $circleEndY)\n" +
"---------------------------\n"
)
path.reset()
path.moveTo(circleStartX, circleStartY)
path.quadraticBezierTo(controlX, controlY, bubbleEndX, bubbleEndY)
path.lineTo(bubbleStartX, bubbleStartY)
path.quadraticBezierTo(controlX, controlY, circleEndX, circleEndY)
drawPath(path, Color.Red)
}
//画可以拖动的圆
if (!dragStatus.onDragComplete) {
drawCircle(Color.Red, bubbleRadius, Offset(offsetX, offsetY))
textWidth = textLayoutResult.size.width.toFloat()
textHeight = textLayoutResult.size.width.toFloat()
drawText(
textLayoutResult, Color.White, topLeft = Offset(
offsetX - textWidth / 2,
offsetY - textHeight / 2
)
)
}
})
}
}