Jetpack Compose Canvas分段环形图及关联图标点击检测
解决方案
一、确定点击的具体环形分段
你已经实现了触摸是否在环形区域内的判断,接下来只需要计算触摸点对应的角度,再匹配到对应的分段即可:
- 完善触摸角度计算:确保
calculateTouchAngle方法能正确将触摸坐标转换为相对于环形图起始角度的角度值,适配Compose Canvas的坐标系(从右侧0°顺时针旋转):
internal fun PointerInputScope.calculateTouchAngle(chartStartAngle: Float, xPos: Float, yPos: Float): Float { val center = size.center // 转换为以画布中心为原点的坐标 val dx = xPos - center.x val dy = yPos - center.y // 计算弧度并转角度,转换为0°-360°范围 var angleRad = atan2(dy, dx) var angleDeg = Math.toDegrees(angleRad).toFloat() if (angleDeg < 0) angleDeg += 360f // 偏移环形图的起始角度,统一到分段的相对角度 angleDeg -= chartStartAngle if (angleDeg < 0) angleDeg += 360f return angleDeg }
- 匹配触摸角度到分段:在触摸回调中,遍历所有分段,检查触摸角度是否落在当前分段的有效角度区间内(扣除分段间的
strokeAngle间隔):
detectTapGestures { position: Offset -> val isSegmentTouched = this.isSegmentTouched( pointerInputScope = this, position = position, circleDiameter = maxWidth, strokeWidth = strokeWidth ) if (isSegmentTouched) { val touchAngle = this.calculateTouchAngle( chartStartAngle = startAngle, xPos = position.x, yPos = position.y ) var currentRelativeAngle = 0f val targetSegment = spendingChartItems.firstOrNull { item -> // 分段实际有效区间:扣除左右两侧的strokeAngle间隔 val segmentStart = currentRelativeAngle + strokeAngle val segmentEnd = currentRelativeAngle + item.archAngle - strokeAngle // 判断触摸角度是否在当前分段区间内 val isMatch = touchAngle in segmentStart..segmentEnd currentRelativeAngle += item.archAngle isMatch } targetSegment?.let { // 处理分段点击逻辑,比如高亮或跳转 // navigateToSegmentDetail(it) } } }
二、检测分段旁关联图标的点击并跳转
要检测图标点击,需要先计算每个图标的实际绘制区域,再在触摸时判断点是否落在该区域内:
- 保存图标点击区域信息:创建数据类存储图标对应的分段和绘制矩形,在绘制阶段计算并保存:
data class IconHitArea(val item: SpendingRingItem, val rect: Rect)
在Canvas绘制代码中,计算每个图标的Rect并存入列表:
val width = size.width val radius = width / 2f val iconHitAreas = mutableListOf<IconHitArea>() // 存储所有图标点击区域 if (spendingChartItems.isNotEmpty() && totalAmount > 0) { val circumference = 2 * Math.PI * (radius - strokeWidth.toPx() / 2) val strokeAngle = delimiterCoefficient * (strokeWidth.toPx() / circumference * 180f).toFloat() var currentAngle = startAngle spendingChartItems.forEach { spendingChartItem -> // ... 绘制环形分段的代码 ... val iconSizePx = getIconSize( percent = spendingChartItem.adjustedPercentage, minSize = minIconSize, maxSize = maxIconSizeDp ).toPx() // 计算图标中心的角度(分段的中间位置) val iconCenterAngle = currentAngle + spendingChartItem.archAngle / 2f // 计算图标中心坐标(根据实际绘制位置调整间距,这里示例为环形外侧+8dp间距) val iconOffsetRadius = radius + strokeWidth.toPx() / 2f + iconSizePx / 2f + 8.dp.toPx() val iconCenterX = radius + cos(Math.toRadians(iconCenterAngle.toDouble())).toFloat() * iconOffsetRadius val iconCenterY = radius + sin(Math.toRadians(iconCenterAngle.toDouble())).toFloat() * iconOffsetRadius // 生成图标的点击矩形区域 val iconRect = Rect( left = iconCenterX - iconSizePx / 2f, top = iconCenterY - iconSizePx / 2f, right = iconCenterX + iconSizePx / 2f, bottom = iconCenterY + iconSizePx / 2f ) iconHitAreas.add(IconHitArea(spendingChartItem, iconRect)) // ... 绘制图标的代码 ... currentAngle += spendingChartItem.archAngle } }
- 在触摸检测中判断图标点击:优先检查图标点击,避免和分段点击逻辑冲突:
detectTapGestures { position: Offset -> // 先检查是否点击到图标 val clickedIconItem = iconHitAreas.firstOrNull { it.rect.contains(position) }?.item if (clickedIconItem != null) { // 处理图标跳转逻辑 // navigateToIconTarget(clickedIconItem) return@detectTapGestures // 终止后续逻辑,避免同时触发分段点击 } // 再处理分段点击逻辑 val isSegmentTouched = this.isSegmentTouched(...) // ... 分段点击的代码 ... }
注意事项
- 确保
startAngle在角度计算和绘制时完全一致,避免角度匹配错误。 - 若只有一个分段,需单独处理
strokeAngle为0的情况,避免区间计算异常。 - 根据
drawCategoryImage的实际绘制位置,调整iconOffsetRadius的计算值,确保Rect和实际图标区域完全重合。
内容的提问来源于stack exchange,提问作者avram andrei tiberiu
相关产品推荐
相关产品推荐

