You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose Canvas分段环形图及关联图标点击检测

解决方案

一、确定点击的具体环形分段

你已经实现了触摸是否在环形区域内的判断,接下来只需要计算触摸点对应的角度,再匹配到对应的分段即可:

  1. 完善触摸角度计算:确保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
}
  1. 匹配触摸角度到分段:在触摸回调中,遍历所有分段,检查触摸角度是否落在当前分段的有效角度区间内(扣除分段间的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)
        }
    }
}

二、检测分段旁关联图标的点击并跳转

要检测图标点击,需要先计算每个图标的实际绘制区域,再在触摸时判断点是否落在该区域内:

  1. 保存图标点击区域信息:创建数据类存储图标对应的分段和绘制矩形,在绘制阶段计算并保存:
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
    }
}
  1. 在触摸检测中判断图标点击:优先检查图标点击,避免和分段点击逻辑冲突:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 20:23:11