如何绘制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
相关产品推荐
相关产品推荐

