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

如何绘制GPS移动轨迹并控制点位数组大小以保障性能

运动轨迹绘制的性能优化问题

我希望在移动设备上绘制类似三星健康的步行/跑步/徒步轨迹,但目前每次获取新位置就存入ArrayList<LatLng>,再用这个数组绘制轨迹,时间一长数组会变得极大,导致性能问题。想知道三星健康这类应用是怎么解决的——既要在会话结束前保留完整轨迹,又要避免点位数组过大影响性能。


现有代码实现

Activity顶部声明

private var points: ArrayList<LatLng>? = null
private var line: Polyline? = null 

onCreate方法

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    points = ArrayList()
}

onMapReady方法

override fun onMapReady(googleMap: GoogleMap) {
    mMap = googleMap

    mMap.uiSettings.isMyLocationButtonEnabled = false

    try {
        mMap.isMyLocationEnabled = false
        mMap.uiSettings.isZoomControlsEnabled = true

        line = mMap.addPolyline(PolylineOptions())

    } catch (e: SecurityException) {
        e.printStackTrace()
    }
}

添加新位置的initializeMapWithLocation方法

private fun initializeMapWithLocation(userLocation: LatLng) {

    mMap.uiSettings.isMyLocationButtonEnabled = false

    try {
        mMap.isMyLocationEnabled = false
        mMap.uiSettings.isZoomControlsEnabled = true

        val selectedMarkerType = MarkerType.CUSTOM_2

        val markerIcon = if (selectedMarkerType == MarkerType.DEFAULT) {
            BitmapDescriptorFactory.defaultMarker()
        } else {
            val originalBitmap = BitmapFactory.decodeResource(resources, selectedMarkerType.resourceId)
            val width = (originalBitmap.width * selectedMarkerType.scale).toInt()
            val height = (originalBitmap.height * selectedMarkerType.scale).toInt()
            val scaledMarker = Bitmap.createScaledBitmap(originalBitmap, width, height, false)
            BitmapDescriptorFactory.fromBitmap(scaledMarker)
        }

        if (marker == null) {
            // 创建新标记(如果还不存在)
            marker = mMap.addMarker(
                MarkerOptions()
                    .position(userLocation)
                    .icon(markerIcon)
                    .title("Marker Title")
            )

            // 加载自定义信息窗口布局
            val infoWindowView = layoutInflater.inflate(R.layout.custom_info_window, null)

            // 将自定义信息窗口设为标记的Tag
            marker?.tag = infoWindowView

            // 设置标记的自定义InfoWindow适配器
            mMap.setInfoWindowAdapter(CustomInfoWindowAdapter())
        } else {
            // 更新现有标记的位置
            updateMarkerPosition(userLocation)

            if (points != null) {
                points!!.add(userLocation)
                line?.points = points as ArrayList<LatLng>
            } else {
                // 如果points为空则初始化
                points = ArrayList()
                points!!.add(userLocation)
            }

            // 从标记的Tag中获取自定义信息窗口布局
            val infoWindowView = marker?.tag as View

            // 找到布局中的TextView
            val latitudeTextView = infoWindowView.findViewById<TextView>(R.id.latitudeTextView)
            val longitudeTextView = infoWindowView.findViewById<TextView>(R.id.longitudeTextView)

            // 设置信息窗口中的经纬度值
            latitudeTextView.text = "Latitude: ${userLocation.latitude}"
            longitudeTextView.text = "Longitude: ${userLocation.longitude}"

            // 显示信息窗口
            marker?.showInfoWindow()
        }

        if (!initialZoomCompleted) {
            val initialZoom = 5.0F
            val targetZoom = 16.0F
            val zoomSpeed = 500
            animateZoomIn(userLocation, initialZoom, targetZoom, zoomSpeed, false)
            initialZoomCompleted = true
        }
    } catch (e: SecurityException) {
        e.printStackTrace()
    }
}

LocationCallback调用逻辑

private val locationCallback = object : LocationCallback() {
    override fun onLocationResult(locationResult: LocationResult) {
        locationResult.lastLocation?.let { location ->
            // 处理新位置
            val newLocation = LatLng(location.latitude, location.longitude)
            initializeMapWithLocation(newLocation)
            loadingProgressBar.visibility = View.GONE
            loadingMapTextView.visibility = View.GONE
        }
    }
}

优化方案(参考三星健康等同类应用逻辑)

1. 轨迹点抽稀(核心优化手段)

保留完整原始轨迹数据的同时,只维护一个简化的点集合用于地图绘制:

  • 维护两个集合:rawPoints保存所有原始位置(用于会话结束后的统计、持久化),displayPoints保存抽稀后的点(用于绘制)。
  • 常用抽稀规则:
    • 距离阈值:只保留与上一个显示点距离超过X米(比如5米)的点。
    • 时间阈值:只保留与上一个显示点时间间隔超过Y秒(比如3秒)的点。
    • 道格拉斯-普克算法:设定路径偏差阈值,自动去除冗余的非拐点,适合长轨迹的形状保留。

示例代码片段:

private const val SIMPLIFY_THRESHOLD_METERS = 5.0
private val rawPoints = mutableListOf<LatLng>()
private val displayPoints = mutableListOf<LatLng>()

private fun addLocationToTrajectory(newLocation: LatLng) {
    rawPoints.add(newLocation)
    
    if (displayPoints.isEmpty()) {
        displayPoints.add(newLocation)
    } else {
        val lastDisplayPoint = displayPoints.last()
        val distance = SphericalUtil.computeDistanceBetween(lastDisplayPoint, newLocation)
        if (distance >= SIMPLIFY_THRESHOLD_METERS) {
            displayPoints.add(newLocation)
            line?.points = displayPoints
        }
    }
}

2. 按地图缩放级别动态渲染

根据当前地图的缩放级别,切换不同精度的轨迹点集合:

  • 低缩放级别(全局视图):使用最稀疏的点集合。
  • 高缩放级别(近距离视图):使用较精细的点集合或原始点。

示例逻辑:

mMap.setOnCameraIdleListener {
    val zoomLevel = mMap.cameraPosition.zoom
    val currentDisplayPoints = when {
        zoomLevel < 12 -> getSimplifiedPoints(rawPoints, 20.0)
        zoomLevel < 15 -> getSimplifiedPoints(rawPoints, 10.0)
        else -> getSimplifiedPoints(rawPoints, 3.0)
    }
    line?.points = currentDisplayPoints
}

// 道格拉斯-普克算法抽稀实现(可自行查找开源实现)
private fun getSimplifiedPoints(points: List<LatLng>, threshold: Double): List<LatLng> {
    // 省略算法具体实现
}

3. 分段渲染长轨迹

将超长轨迹拆分为多个小段(比如每100个原始点为一段),每段单独维护抽稀后的显示点,使用多个Polyline对象分别绘制,避免单个Polyline的点集合过大导致性能问题。

4. 减少Polyline重绘频率

当前代码每次新增点都直接设置line?.points = points,会触发频繁重绘:

  • 批量更新:每新增3-5个点再更新一次Polyline。
  • 追加新点:如果API支持,使用Polyline.addPoint()方法替代设置整个点集合,减少重绘开销。

内容的提问来源于stack exchange,提问作者Daniel Lip

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 15:59:50