Mapbox Android SDK 10 相机空闲状态检测方案
2024-12-16 20:45:49
检测 Mapbox Android SDK 10 中相机空闲状态
在 Mapbox Android SDK 9 迁移到 SDK 10 的过程中,开发者普遍注意到 OnCameraIdleListener
已被移除。此监听器曾用于检测相机何时停止移动,无论是用户手势操作还是程序化控制。本文将探讨 SDK 10 中实现相机空闲状态检测的替代方案,并提供代码示例和详细步骤。
问题分析
OnCameraIdleListener
的移除意味着需要采用新的机制来判断相机是否处于空闲状态。 SDK 10 提供了 MapChangeListener
接口,该接口包含一个 onMapIdle()
回调方法。但根据开发者反馈,onMapIdle()
的触发过于频繁,即便地图和相机没有移动也会被调用,无法满足精确判断相机空闲状态的需求。
主要原因是 onMapIdle()
关注的是整个地图的渲染状态,而不仅仅是相机的移动。因此,我们需要结合其他方法来更准确地检测相机空闲。
解决方案
1. 基于 CameraAnimationsLifecycleListener
和延时机制
Mapbox SDK 10 的 MapboxMap
对象提供了一个 cameraAnimationsLifecycleListener
属性,可以监听相机动画的生命周期。通过监听动画的结束,并在动画结束后设置一个延时,可以有效地判断相机是否空闲。
原理:
当用户停止操作地图或程序化相机动画结束时,cameraAnimationsLifecycleListener
会触发 onAnimationEnd
回调。由于用户操作或动画结束后相机可能仍有微小的惯性移动,设置延时可以过滤掉这些细微的移动,确保相机真正处于静止状态。
步骤:
- 实现
CameraAnimationsLifecycleListener
接口。 - 在
onAnimationStart
回调中,取消之前的延时任务(如果存在)。 - 在
onAnimationEnd
回调中,设置一个延时任务。 - 延时任务执行时,触发相机空闲事件。
代码示例:
import android.os.Handler
import android.os.Looper
import com.mapbox.maps.CameraAnimationsLifecycleListener
import com.mapbox.maps.MapboxMap
import java.util.Timer
import java.util.TimerTask
class CameraIdleDetector(private val mapboxMap: MapboxMap, private val idleDelay: Long = 500) {
private var handler = Handler(Looper.getMainLooper())
private var idleRunnable: Runnable? = null
private var cameraIdleListener: (() -> Unit)? = null
private val cameraAnimationsLifecycleListener = object : CameraAnimationsLifecycleListener {
override fun onAnimationStart() {
// 取消之前的延时任务
idleRunnable?.let { handler.removeCallbacks(it) }
}
override fun onAnimationEnd() {
startIdleTimer()
}
}
init {
mapboxMap.cameraAnimationsLifecycleListener = cameraAnimationsLifecycleListener
}
private fun startIdleTimer() {
idleRunnable = Runnable {
cameraIdleListener?.invoke()
}
idleRunnable?.let{
handler.postDelayed(it, idleDelay)
}
}
fun setOnCameraIdleListener(listener: () -> Unit) {
cameraIdleListener = listener
}
fun cancel() {
mapboxMap.cameraAnimationsLifecycleListener=null
}
}
使用方法:
val cameraIdleDetector = CameraIdleDetector(mapboxMap)
cameraIdleDetector.setOnCameraIdleListener {
// 相机空闲时的操作,例如加载新的地图数据或更新UI
Log.i("Camera", "Camera is Idle")
}
override fun onDestroy() {
cameraIdleDetector.cancel()
super.onDestroy()
}
安全提示:
- 延时时间
idleDelay
需要根据实际情况调整,过短可能导致误判,过长则会影响用户体验。 CameraIdleDetector
类应该在Activity
或Fragment
的onDestroy
方法中取消监听,以防止内存泄漏。
2. 利用 MapboxMap.queryRenderedFeatures
进行连续采样
可以通过定时器或 Choreographer
以较高的频率获取相机视野内的特征,并比较连续采样中特征的位置变化。 如果一段时间内特征的位置没有变化或变化小于某个阈值,则认为相机处于空闲状态。
原理:
如果相机持续移动,视野内特征的屏幕坐标也会不断变化。通过连续采样和位置比较,可以判断相机是否静止。
步骤:
- 创建一个定时器或使用
Choreographer
来触发采样。 - 在每次采样时,使用
MapboxMap.queryRenderedFeatures
获取当前视野内的特征。 - 比较当前采样和上一次采样的特征位置。
- 如果位置变化小于阈值,则增加计数。
- 如果连续多次采样位置变化都小于阈值,则认为相机空闲。
代码示例 (使用Choreographer):
import android.view.Choreographer
import com.mapbox.geojson.Point
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.ScreenCoordinate
import kotlin.math.abs
class ContinuousSamplingCameraIdleDetector(
private val mapboxMap: MapboxMap,
private val idleThreshold: Double = 5.0,
private val idleFrameCount: Int = 3
) {
private var lastFeaturePositions: MutableList<Point>? = null
private var idleFrameCounter = 0
private var cameraIdleListener: (() -> Unit)? = null
private val choreographerCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
checkCameraIdle()
Choreographer.getInstance().postFrameCallback(this)
}
}
init {
startSampling()
}
private fun startSampling() {
Choreographer.getInstance().postFrameCallback(choreographerCallback)
}
private fun checkCameraIdle() {
val currentFeaturePositions = mutableListOf<Point>()
val screenCoordinates= mutableListOf<ScreenCoordinate>()
screenCoordinates.add(ScreenCoordinate(mapboxMap.width.toFloat()/2,mapboxMap.height.toFloat()/2 ))
mapboxMap.queryRenderedFeatures(screenCoordinates).forEach { feature ->
feature.geometry()?.let { geometry ->
if (geometry is Point) {
currentFeaturePositions.add(geometry)
}
}
}
if (lastFeaturePositions == null || currentFeaturePositions.isEmpty()) {
lastFeaturePositions = currentFeaturePositions
return
}
val averageMovement = calculateAverageMovement(lastFeaturePositions!!,currentFeaturePositions)
if(averageMovement < idleThreshold){
idleFrameCounter++
if(idleFrameCounter >=idleFrameCount){
cameraIdleListener?.invoke()
stopSampling()
}
}
else{
idleFrameCounter=0
}
lastFeaturePositions = currentFeaturePositions
}
private fun calculateAverageMovement(previousPositions: List<Point>, currentPositions: List<Point>): Double {
if (previousPositions.isEmpty() || currentPositions.isEmpty()||currentPositions.size!=previousPositions.size) {
return Double.MAX_VALUE
}
var totalMovement=0.0
for(i in previousPositions.indices){
val previousPosition = previousPositions[i]
val currentPosition = currentPositions[i]
val projectedPoint = mapboxMap.projection.project(currentPosition)
val previousProjectedPoint = mapboxMap.projection.project(previousPosition)
totalMovement+= calculateDistance(projectedPoint.x.toDouble(),projectedPoint.y.toDouble(),
previousProjectedPoint.x.toDouble(),previousProjectedPoint.y.toDouble() )
}
return totalMovement/currentPositions.size
}
private fun calculateDistance(x1:Double,y1:Double,x2:Double,y2:Double):Double{
return abs(x1-x2)+abs(y1-y2)
}
fun setOnCameraIdleListener(listener: () -> Unit) {
cameraIdleListener = listener
}
private fun stopSampling() {
Choreographer.getInstance().removeFrameCallback(choreographerCallback)
}
fun cancel() {
stopSampling()
}
}
使用方法:
val cameraIdleDetector = ContinuousSamplingCameraIdleDetector(mapboxMap)
cameraIdleDetector.setOnCameraIdleListener {
// 相机空闲时的操作
Log.i("Camera", "Camera is Idle")
}
override fun onDestroy() {
cameraIdleDetector.cancel()
super.onDestroy()
}
安全提示:
- `