You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Google Maps Android API:圆形替代标记的性能与尺寸问题求助

优化Google Maps大量圆形标记的性能与缩放控制方案

我之前在处理类似的大规模Google Maps标记需求时踩过不少坑,结合Google Maps团队的最佳实践,给你针对性的解决方案:

一、解决卡顿加剧问题

你的核心问题是频繁创建/销毁Circle对象以及冗余的渲染/触摸事件处理,可以从以下几点优化:

1. 复用Circle对象,避免重复创建

维护一个缓存池存储已创建的内外圆对,每次更新时优先复用现有对象,而非重新添加:

// 缓存POI对应的内外圆,key为POI的id
private val circleCache = mutableMapOf<String, Pair<Circle, Circle>>()

fun updateCircles(pois: List<Poi>) {
    // 标记需要删除的旧Circle
    val toRemoveIds = circleCache.keys.toMutableSet()
    
    pois.forEach { poi ->
        toRemoveIds.remove(poi.id) // 保留当前需要显示的POI对应的Circle
        val zoomLevel = map.cameraPosition.zoom.toDouble()
        val isSelected = selectedPoi?.id == poi.id
        val markersVisible = true // 替换为你的实际变量

        circleCache[poi.id]?.let { (innerCircle, outerCircle) ->
            // 复用现有Circle,更新属性即可
            updateInnerCircle(innerCircle, poi, isSelected, markersVisible)
            updateOuterCircle(outerCircle, poi, zoomLevel, isSelected, markersVisible)
        } ?: run {
            // 无缓存时创建新Circle并加入缓存
            val innerCircle = map.addCircle(createInnerCircleOptions(poi, isSelected, markersVisible)).apply { tag = poi }
            val outerCircle = map.addCircle(createOuterCircleOptions(poi, zoomLevel, isSelected, markersVisible)).apply { tag = poi }
            circleCache[poi.id] = Pair(innerCircle, outerCircle)
        }
    }

    // 删除不再需要的Circle,释放资源
    toRemoveIds.forEach { id ->
        circleCache[id]?.let { (inner, outer) ->
            inner.remove()
            outer.remove()
        }
        circleCache.remove(id)
    }
}

private fun updateInnerCircle(circle: Circle, poi: Poi, isSelected: Boolean, visible: Boolean) {
    circle.apply {
        center = poi.location.toLatLng()
        fillColor = colorForPoi(poi, isSelected)
        zIndex = if (isSelected) INNER_CIRCLE_SELECTED_Z_INDEX else INNER_CIRCLE_Z_INDEX
        this.visible = visible
        clickable = !isSelected // 仅保留内圆的点击事件
    }
}

private fun updateOuterCircle(circle: Circle, poi: Poi, zoomLevel: Double, isSelected: Boolean, visible: Boolean) {
    circle.apply {
        center = poi.location.toLatLng()
        fillColor = if (isSelected) context.color(R.color.colorPrimary) else context.color(R.color.black_30)
        zIndex = if (isSelected) OUTER_CIRCLE_SELECTED_Z_INDEX else OUTER_CIRCLE_Z_INDEX
        radius = calculateClampedRadius(zoomLevel, isSelected, poi.location.lat)
        this.visible = visible
        clickable = false // 关闭外圆点击,减少触摸事件开销
    }
}

2. 减少冗余渲染与事件处理

  • 只给内圆设置clickable=true,外圆关闭点击,避免重复处理触摸事件;
  • 统一zIndex层级,避免每个Circle使用独立的zIndex导致绘制批次增加;
  • 低缩放级别下简化显示:比如当zoomLevel < 12时,隐藏外圆,只显示内圆,减少渲染压力。

3. 后台线程预处理计算

如果POI数量极大,可将颜色计算、半径计算等逻辑放到后台线程执行,再切换到UI线程更新Circle属性,避免阻塞UI。

二、解决缩放时圆形尺寸失控问题

Circle的radius是米为单位,会随纬度和缩放级别变化导致屏幕显示大小不稳定,我们可以将屏幕像素尺寸转换为米,并添加最小/最大限制:

// 计算带边界限制的半径(基于屏幕像素转换为米)
private fun calculateClampedRadius(zoomLevel: Double, isSelected: Boolean, lat: Double): Double {
    // 定义屏幕像素的最小/最大尺寸
    val (minPixel, maxPixel, basePixel) = if (isSelected) {
        Triple(16f, 24f, 18f) // 选中状态:固定基础尺寸,限制范围
    } else {
        // 未选中状态:随缩放级别动态调整,同时限制范围
        val dynamicPixel = 12f + (zoomLevel - 10) * 1.5f
        Triple(10f, 22f, dynamicPixel)
    }
    val clampedPixel = basePixel.coerceIn(minPixel, maxPixel)
    return pixelToMeters(clampedPixel, lat, zoomLevel)
}

// 将屏幕像素转换为地图上的米数
private fun pixelToMeters(pixel: Float, lat: Double, zoom: Double): Double {
    val metersPerPixel = 156543.03392 * Math.cos(Math.toRadians(lat)) / Math.pow(2.0, zoom)
    return pixel * metersPerPixel
}

这样无论地图缩放或纬度变化,圆形的屏幕显示大小都会稳定在你定义的范围内,不会出现过大或过小的情况。

额外优化建议

  • 确保MapView开启硬件加速(默认开启,可在Manifest中确认<application android:hardwareAccelerated="true">);
  • 避免在地图相机移动时实时更新所有Circle,可监听OnCameraIdleListener,在相机停止移动后再批量更新,减少频繁渲染。

内容的提问来源于stack exchange,提问作者Matthias Siegmund

火山引擎 最新活动