From b33cfca741f613bc45975a512a15de8ce946f9f7 Mon Sep 17 00:00:00 2001 From: Thomas Han Date: Tue, 2 Jun 2026 10:03:49 +1200 Subject: [PATCH 1/3] update CameraChangeTracker so that it is more robust against multiple gestures being tracked that could lead to race conditions --- .../rnmbx/components/mapview/RNMBXMapView.kt | 7 +++-- .../mapview/helpers/CameraChangeTracker.kt | 26 +++++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index a3ab205f25..7a85ad4e8c 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -315,11 +315,10 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie fun mapGestureBegin(type:MapGestureType, gesture: T) { isGestureActive = true - mCameraChangeTracker.setReason(CameraChangeReason.USER_GESTURE) + mCameraChangeTracker.setReason(type, CameraChangeReason.USER_GESTURE) handleMapChangedEvent(EventTypes.REGION_WILL_CHANGE) } fun mapGesture(type: MapGestureType, gesture: T): Boolean { - mCameraChangeTracker.setReason(CameraChangeReason.USER_GESTURE) handleMapChangedEvent(EventTypes.REGION_IS_CHANGING) return false } @@ -425,7 +424,7 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie val didChangeEvent = MapChangeEvent(this, EventTypes.REGION_DID_CHANGE, makeRegionPayload(isAnimated)) mManager.handleEvent(didChangeEvent) - mCameraChangeTracker.setReason(CameraChangeReason.NONE) + mCameraChangeTracker.clear() } private fun removeAllFeaturesFromMap(reason: RemovalReason) { @@ -781,7 +780,7 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie fun sendRegionDidChangeEvent() { handleMapChangedEvent(EventTypes.REGION_DID_CHANGE) - mCameraChangeTracker.setReason(CameraChangeReason.NONE) + mCameraChangeTracker.clear() } private fun handleMapChangedEvent(eventType: String) { diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/CameraChangeTracker.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/CameraChangeTracker.kt index 58a83bde9a..a6d9956e2c 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/CameraChangeTracker.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/CameraChangeTracker.kt @@ -1,23 +1,33 @@ package com.rnmapbox.rnmbx.components.mapview.helpers +import com.rnmapbox.rnmbx.components.mapview.MapGestureType + enum class CameraChangeReason { - NONE, USER_GESTURE, DEVELOPER_ANIMATION, SDK_ANIMATION } class CameraChangeTracker { - private var reason : CameraChangeReason = CameraChangeReason.NONE + private val reasonByGesture = mutableMapOf() var isAnimating = false - fun setReason(reason: CameraChangeReason) { - this.reason = reason + + fun setReason(type: MapGestureType, reason: CameraChangeReason) { + reasonByGesture[type] = reason + } + + fun clear() { + reasonByGesture.clear() + } + + fun clearReason(type: MapGestureType) { + reasonByGesture.remove(type) } val isUserInteraction: Boolean - get() = reason == CameraChangeReason.USER_GESTURE || reason == CameraChangeReason.DEVELOPER_ANIMATION + get() = reasonByGesture.values.any { it == CameraChangeReason.USER_GESTURE } val isAnimated: Boolean - get() = reason == CameraChangeReason.DEVELOPER_ANIMATION || reason == CameraChangeReason.SDK_ANIMATION + get() = reasonByGesture.values.any { it == CameraChangeReason.DEVELOPER_ANIMATION || it == CameraChangeReason.SDK_ANIMATION } val isEmpty: Boolean - get() = reason == CameraChangeReason.NONE -} \ No newline at end of file + get() = reasonByGesture.isEmpty() +} From 064cf64a63483b8a1fab3f5b5eaeb79d50463ade Mon Sep 17 00:00:00 2001 From: Thomas Han Date: Tue, 2 Jun 2026 10:05:47 +1200 Subject: [PATCH 2/3] refactor out MapSteadyDetector from RNMBXCameraGestureObserver so that map steady event detection is decoupled and can be minimally reused from RNMBXMapView --- .../rnmbx/components/mapview/RNMBXMapView.kt | 9 + .../mapview/helpers/MapSteadyDetector.kt | 177 +++++++++++ .../events/RNMBXCameraGestureObserver.kt | 281 +++--------------- 3 files changed, 224 insertions(+), 243 deletions(-) create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index 7a85ad4e8c..1dd14f2374 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -47,6 +47,7 @@ import com.rnmapbox.rnmbx.components.location.LocationComponentManager import com.rnmapbox.rnmbx.components.location.RNMBXNativeUserLocation import com.rnmapbox.rnmbx.components.mapview.helpers.CameraChangeReason import com.rnmapbox.rnmbx.components.mapview.helpers.CameraChangeTracker +import com.rnmapbox.rnmbx.components.mapview.helpers.MapSteadyDetector import com.rnmapbox.rnmbx.components.styles.layers.RNMBXLayer import com.rnmapbox.rnmbx.components.styles.light.RNMBXLight import com.rnmapbox.rnmbx.components.styles.sources.RNMBXSource @@ -156,6 +157,7 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie private val mFeatures = mutableListOf() private var mQueuedFeatures: MutableList? = ArrayList() private val mCameraChangeTracker = CameraChangeTracker() + private var mMapSteadyDetector: MapSteadyDetector? = null private var mPreferredFrameRate: Int? = null private var mMaxPitch: Double? = null private lateinit var mMap: MapboxMap @@ -240,6 +242,13 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie handleMapChangedEvent(EventTypes.MAP_IDLE); }) + mMapSteadyDetector = MapSteadyDetector(map).apply { + onSteady = { idleDurationMs, lastGestureType -> + mCameraChangeTracker.clear() + } + attach() + } + val gesturesPlugin: GesturesPlugin = mapView.gestures gesturesPlugin.addOnMapLongClickListener(_this) gesturesPlugin.addOnMapClickListener(_this) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt new file mode 100644 index 0000000000..5f78d8ca55 --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt @@ -0,0 +1,177 @@ +package com.rnmapbox.rnmbx.components.mapview.helpers + +import android.animation.ValueAnimator +import android.os.Handler +import android.os.Looper +import com.mapbox.android.gestures.MoveGestureDetector +import com.mapbox.android.gestures.RotateGestureDetector +import com.mapbox.android.gestures.ShoveGestureDetector +import com.mapbox.android.gestures.StandardScaleGestureDetector +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.plugin.animation.CameraAnimationsLifecycleListener +import com.mapbox.maps.plugin.animation.CameraAnimatorType +import com.mapbox.maps.plugin.gestures.OnMoveListener +import com.mapbox.maps.plugin.gestures.OnRotateListener +import com.mapbox.maps.plugin.gestures.OnScaleListener +import com.mapbox.maps.plugin.gestures.OnShoveListener + +class MapSteadyDetector( + private val mapboxMap: MapboxMap, + var quietPeriodMs: Double = 200.0, + var maxIntervalMs: Double? = null, +) { + var onSteady: ((idleDurationMs: Double, lastGestureType: String?) -> Unit)? = null + var onTimeout: ((lastGestureType: String?) -> Unit)? = null + + private val handler = Handler(Looper.getMainLooper()) + var activeAnimations: Int = 0 + private set + var isGestureActive: Boolean = false + private set + private var lastGestureType: String? = null + var lastTransitionEndedAtMs: Double? = null + private set + private var quietRunnable: Runnable? = null + private var timeoutRunnable: Runnable? = null + + private fun nowMs(): Double = System.currentTimeMillis().toDouble() + + private val canEmitSteady: Boolean + get() = activeAnimations == 0 && !isGestureActive && lastTransitionEndedAtMs != null + + fun attach() { + mapboxMap.cameraAnimationsPlugin { + addCameraAnimationsLifecycleListener(object : CameraAnimationsLifecycleListener { + override fun onAnimatorStarting(type: CameraAnimatorType, animator: ValueAnimator, owner: String?) { + if (owner != GESTURES_OWNER) return + activeAnimations++ + lastTransitionEndedAtMs = null + markActivity() + } + + override fun onAnimatorEnding(type: CameraAnimatorType, animator: ValueAnimator, owner: String?) { + if (owner != GESTURES_OWNER) return + handleAnimatorEnd() + } + + override fun onAnimatorCancelling(type: CameraAnimatorType, animator: ValueAnimator, owner: String?) { + if (owner != GESTURES_OWNER) return + handleAnimatorEnd() + } + + override fun onAnimatorInterrupting( + type: CameraAnimatorType, + runningAnimator: ValueAnimator, + runningAnimatorOwner: String?, + newAnimator: ValueAnimator, + newAnimatorOwner: String? + ) {} + }) + } + + mapboxMap.gesturesPlugin { + addOnMoveListener(object : OnMoveListener { + override fun onMoveBegin(detector: MoveGestureDetector) { handleGestureBegin("move") } + override fun onMove(detector: MoveGestureDetector): Boolean = false + override fun onMoveEnd(detector: MoveGestureDetector) { handleGestureEnd("move") } + }) + addOnScaleListener(object : OnScaleListener { + override fun onScaleBegin(detector: StandardScaleGestureDetector) { handleGestureBegin("scale") } + override fun onScale(detector: StandardScaleGestureDetector) {} + override fun onScaleEnd(detector: StandardScaleGestureDetector) { handleGestureEnd("scale") } + }) + addOnRotateListener(object : OnRotateListener { + override fun onRotateBegin(detector: RotateGestureDetector) { handleGestureBegin("rotate") } + override fun onRotate(detector: RotateGestureDetector) {} + override fun onRotateEnd(detector: RotateGestureDetector) { handleGestureEnd("rotate") } + }) + addOnShoveListener(object : OnShoveListener { + override fun onShoveBegin(detector: ShoveGestureDetector) { handleGestureBegin("shove") } + override fun onShove(detector: ShoveGestureDetector) {} + override fun onShoveEnd(detector: ShoveGestureDetector) { handleGestureEnd("shove") } + }) + } + } + + fun detach() { + cancelQuietTimer() + cancelTimeoutTimer() + } + + private fun cancelQuietTimer() { + quietRunnable?.let { handler.removeCallbacks(it) } + quietRunnable = null + } + + private fun cancelTimeoutTimer() { + timeoutRunnable?.let { handler.removeCallbacks(it) } + timeoutRunnable = null + } + + private fun scheduleQuietCheck() { + cancelQuietTimer() + if (quietPeriodMs <= 0) { + maybeEmitSteady() + return + } + val runnable = Runnable { maybeEmitSteady() } + handler.postDelayed(runnable, quietPeriodMs.toLong()) + quietRunnable = runnable + } + + private fun scheduleTimeoutTimer() { + if (timeoutRunnable != null) return + val delay = maxIntervalMs ?: return + if (delay <= 0) return + val runnable = Runnable { + timeoutRunnable = null + onTimeout?.invoke(lastGestureType) + scheduleTimeoutTimer() + } + handler.postDelayed(runnable, delay.toLong()) + timeoutRunnable = runnable + } + + private fun markActivity(gestureType: String? = null) { + if (gestureType != null) lastGestureType = gestureType + scheduleQuietCheck() + scheduleTimeoutTimer() + } + + private fun maybeEmitSteady() { + if (!canEmitSteady) return + val lastEnd = lastTransitionEndedAtMs ?: return + val sinceEnd = nowMs() - lastEnd + if (sinceEnd < quietPeriodMs) return + cancelQuietTimer() + cancelTimeoutTimer() + val gesture = lastGestureType + onSteady?.invoke(sinceEnd, gesture) + lastGestureType = null + } + + private fun handleAnimatorEnd() { + activeAnimations-- + if (activeAnimations < 0) activeAnimations = 0 + lastTransitionEndedAtMs = nowMs() + scheduleQuietCheck() + } + + private fun handleGestureBegin(type: String) { + isGestureActive = true + lastGestureType = type + lastTransitionEndedAtMs = null + markActivity(lastGestureType) + } + + private fun handleGestureEnd(type: String) { + lastGestureType = type + isGestureActive = false + lastTransitionEndedAtMs = nowMs() + markActivity(lastGestureType) + } + + companion object { + private const val GESTURES_OWNER = "Maps-Gestures" + } +} diff --git a/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt b/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt index dbeced9976..3d1384b1ee 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt @@ -1,23 +1,11 @@ package com.rnmapbox.rnmbx.events -import android.animation.ValueAnimator import android.content.Context -import android.os.Handler -import android.os.Looper import android.util.Log -import com.mapbox.android.gestures.MoveGestureDetector -import com.mapbox.android.gestures.RotateGestureDetector -import com.mapbox.android.gestures.ShoveGestureDetector -import com.mapbox.android.gestures.StandardScaleGestureDetector -import com.mapbox.maps.plugin.animation.CameraAnimationsLifecycleListener -import com.mapbox.maps.plugin.animation.CameraAnimatorType -import com.mapbox.maps.plugin.gestures.OnMoveListener -import com.mapbox.maps.plugin.gestures.OnRotateListener -import com.mapbox.maps.plugin.gestures.OnScaleListener -import com.mapbox.maps.plugin.gestures.OnShoveListener import com.rnmapbox.rnmbx.components.AbstractMapFeature import com.rnmapbox.rnmbx.components.RemovalReason import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView +import com.rnmapbox.rnmbx.components.mapview.helpers.MapSteadyDetector class RNMBXCameraGestureObserver( private val mContext: Context, @@ -30,24 +18,16 @@ class RNMBXCameraGestureObserver( override var requiresStyleLoad: Boolean = false - private val handler = Handler(Looper.getMainLooper()) - private var activeAnimations: Int = 0 - private var isGestureActive: Boolean = false - private var lastGestureType: String? = null - private var lastTransitionEndedAtMs: Double? = null - private var quietRunnable: Runnable? = null - private var timeoutRunnable: Runnable? = null + private var detector: MapSteadyDetector? = null - private val quietMs: Double get() = quietPeriodMs ?: 200.0 - private val maxMs: Double? get() = maxIntervalMs - - private fun nowMs(): Double = System.currentTimeMillis().toDouble() - private fun timestamp(): Double = nowMs() - - private val canEmitSteady: Boolean - get() = activeAnimations == 0 && !isGestureActive && lastTransitionEndedAtMs != null + private fun debugLog(message: String) { + Log.d( + LOG_TAG, + "$message; activeAnimations=${detector?.activeAnimations} isGestureActive=${detector?.isGestureActive} lastTransitionEnd=${detector?.lastTransitionEndedAtMs ?: -1}" + ) + } - private fun normalizeGestureType(type: String): String { + private fun normalizeGestureType(type: String?): String? { return when (type) { "move" -> "pan" "scale" -> "pinch" @@ -57,237 +37,52 @@ class RNMBXCameraGestureObserver( } } - private fun debugLog(message: String) { - Log.d( - LOG_TAG, - "$message; activeAnimations=$activeAnimations isGestureActive=$isGestureActive lastTransitionEnd=${lastTransitionEndedAtMs ?: -1}" - ) - } - - private fun scheduleTimer(delay: Double, task: Runnable): Runnable { - handler.removeCallbacks(task) - if (delay > 0) { - handler.postDelayed(task, delay.toLong()) - } - return task - } - - private fun cancelQuietTimer() { - quietRunnable?.let { handler.removeCallbacks(it) } - quietRunnable = null - } - - private fun cancelTimeoutTimer() { - timeoutRunnable?.let { handler.removeCallbacks(it) } - timeoutRunnable = null - } - - private fun scheduleQuietCheck() { - cancelQuietTimer() - val delay = quietMs - if (delay <= 0) { - maybeEmitSteady() - return - } - debugLog("scheduleQuietCheck in ${delay.toInt()}ms") - val runnable = Runnable { - debugLog("quiet timer fired") - maybeEmitSteady() - } - quietRunnable = scheduleTimer(delay, runnable) - } - - private fun scheduleTimeout() { - if (timeoutRunnable != null) return - val delay = maxMs ?: return - val runnable = Runnable { - timeoutRunnable = null - emitTimeout() - } - timeoutRunnable = scheduleTimer(delay, runnable) - } - - private fun markActivity(gestureType: String? = null) { - if (gestureType != null) lastGestureType = gestureType - scheduleQuietCheck() - scheduleTimeout() - } - - private fun maybeEmitSteady() { - if (!canEmitSteady) return - val lastEnd = lastTransitionEndedAtMs ?: return - val sinceEnd = nowMs() - lastEnd - if (sinceEnd < quietMs) return - emitSteady(sinceEnd) - } - - private fun emitSteady(idleDurationMs: Double) { - cancelQuietTimer() - cancelTimeoutTimer() - val gesture = lastGestureType - debugLog("EMIT steady idleDurationMs=$idleDurationMs lastGestureType=$gesture") - mManager.handleEvent( - MapSteadyEvent.make(this, "steady", idleDurationMs, gesture) - ) - lastGestureType = null - } - - private fun emitTimeout() { - cancelQuietTimer() - debugLog("EMIT timeout lastGestureType=$lastGestureType") - mManager.handleEvent( - MapSteadyEvent.make(this, "timeout", null, lastGestureType) - ) - scheduleTimeout() - } - override fun addToMap(mapView: RNMBXMapView) { super.addToMap(mapView) if (!hasOnMapSteady) return mapView.getMapAsync { mapboxMap -> - // Camera animations lifecycle - mapboxMap.cameraAnimationsPlugin { - this.addCameraAnimationsLifecycleListener(object : CameraAnimationsLifecycleListener { - override fun onAnimatorStarting( - type: CameraAnimatorType, - animator: ValueAnimator, - owner: String? - ) { - if (owner != "Maps-Gestures") return - activeAnimations++ - lastTransitionEndedAtMs = null - markActivity() - debugLog("camera animator started") - } - - override fun onAnimatorEnding( - type: CameraAnimatorType, - animator: ValueAnimator, - owner: String? - ) { - if (owner != "Maps-Gestures") return - handleAnimatorEnd() - } - - override fun onAnimatorCancelling( - type: CameraAnimatorType, - animator: ValueAnimator, - owner: String? - ) { - if (owner != "Maps-Gestures") return - handleAnimatorEnd() - } - - override fun onAnimatorInterrupting( - type: CameraAnimatorType, - runningAnimator: ValueAnimator, - runningAnimatorOwner: String?, - newAnimator: ValueAnimator, - newAnimatorOwner: String? - ) { - // Interruptions are handled by the ending/starting callbacks - } - }) + val det = MapSteadyDetector( + mapboxMap = mapboxMap, + quietPeriodMs = quietPeriodMs ?: 200.0, + maxIntervalMs = maxIntervalMs, + ) + det.onSteady = { idleDurationMs, lastGestureType -> + debugLog("EMIT steady idleDurationMs=$idleDurationMs lastGestureType=$lastGestureType") + mManager.handleEvent( + MapSteadyEvent.make( + this, + "steady", + idleDurationMs, + normalizeGestureType(lastGestureType) + ) + ) } - - // Gesture listeners - mapboxMap.gesturesPlugin { - this.addOnMoveListener(object : OnMoveListener { - override fun onMoveBegin(detector: MoveGestureDetector) { - handleGestureBegin("move") - } - - override fun onMove(detector: MoveGestureDetector): Boolean { - return false - } - - override fun onMoveEnd(detector: MoveGestureDetector) { - handleGestureEnd("move") - } - }) - - this.addOnScaleListener(object : OnScaleListener { - override fun onScaleBegin(detector: StandardScaleGestureDetector) { - handleGestureBegin("scale") - } - - override fun onScale(detector: StandardScaleGestureDetector) { - } - - override fun onScaleEnd(detector: StandardScaleGestureDetector) { - handleGestureEnd("scale") - } - }) - - this.addOnRotateListener(object : OnRotateListener { - override fun onRotateBegin(detector: RotateGestureDetector) { - handleGestureBegin("rotate") - } - - override fun onRotate(detector: RotateGestureDetector) { - } - - override fun onRotateEnd(detector: RotateGestureDetector) { - handleGestureEnd("rotate") - } - }) - - this.addOnShoveListener(object : OnShoveListener { - override fun onShoveBegin(detector: ShoveGestureDetector) { - handleGestureBegin("shove") - } - - override fun onShove(detector: ShoveGestureDetector) { - } - - override fun onShoveEnd(detector: ShoveGestureDetector) { - handleGestureEnd("shove") - } - }) + det.onTimeout = { lastGestureType -> + debugLog("EMIT timeout lastGestureType=$lastGestureType") + mManager.handleEvent( + MapSteadyEvent.make( + this, + "timeout", + null, + normalizeGestureType(lastGestureType) + ) + ) } - + det.attach() + detector = det debugLog("addToMap and subscribed to gestures") } } override fun removeFromMap(mapView: RNMBXMapView, reason: RemovalReason): Boolean { debugLog("removeFromMap and unsubscribed from gestures") - cancelQuietTimer() - cancelTimeoutTimer() + detector?.detach() + detector = null return super.removeFromMap(mapView, reason) } - private fun handleAnimatorEnd() { - activeAnimations-- - if (activeAnimations < 0) { - Log.w(LOG_TAG, "WARNING: activeAnimations went negative, resetting to 0") - activeAnimations = 0 - } - lastTransitionEndedAtMs = nowMs() - scheduleQuietCheck() - debugLog("camera animator ended") - } - - private fun handleGestureBegin(type: String) { - isGestureActive = true - lastGestureType = normalizeGestureType(type) - lastTransitionEndedAtMs = null - markActivity(lastGestureType) - debugLog("gesture didBegin type=$lastGestureType") - } - - private fun handleGestureEnd(type: String) { - lastGestureType = normalizeGestureType(type) - // On Android, gesture end callbacks fire AFTER animations complete - // So we can mark the gesture as inactive and transition as ended - isGestureActive = false - lastTransitionEndedAtMs = nowMs() - markActivity(lastGestureType) - debugLog("gesture didEnd type=$lastGestureType -> isGestureActive=$isGestureActive") - } - companion object { const val LOG_TAG = "RNMBXCameraGestureObserver" } From 928330fa40a563281e79b832d3011f122227a4bd Mon Sep 17 00:00:00 2001 From: Thomas Han Date: Tue, 2 Jun 2026 10:14:33 +1200 Subject: [PATCH 3/3] - refactor MapGestureType so that it is a reusable enum across different kotlin components - refactor MapSteadyDetector so that it uses enum instead of magic strings --- .../components/mapview/MapGestureType.kt | 5 ++++ .../rnmbx/components/mapview/RNMBXMapView.kt | 4 --- .../mapview/helpers/MapSteadyDetector.kt | 29 ++++++++++--------- .../events/RNMBXCameraGestureObserver.kt | 16 +++++----- 4 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/mapview/MapGestureType.kt diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/MapGestureType.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/MapGestureType.kt new file mode 100644 index 0000000000..b38f212443 --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/MapGestureType.kt @@ -0,0 +1,5 @@ +package com.rnmapbox.rnmbx.components.mapview + +enum class MapGestureType { + Move, Scale, Rotate, Fling, Shove +} diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index 1dd14f2374..0a83546c6c 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -92,10 +92,6 @@ data class OrnamentSettings( var position: Int = -1 ) -enum class MapGestureType { - Move,Scale,Rotate,Fling,Shove -} - fun interface Cancelable { fun cancel() } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt index 5f78d8ca55..fc9744b7b3 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/helpers/MapSteadyDetector.kt @@ -14,21 +14,22 @@ import com.mapbox.maps.plugin.gestures.OnMoveListener import com.mapbox.maps.plugin.gestures.OnRotateListener import com.mapbox.maps.plugin.gestures.OnScaleListener import com.mapbox.maps.plugin.gestures.OnShoveListener +import com.rnmapbox.rnmbx.components.mapview.MapGestureType class MapSteadyDetector( private val mapboxMap: MapboxMap, var quietPeriodMs: Double = 200.0, var maxIntervalMs: Double? = null, ) { - var onSteady: ((idleDurationMs: Double, lastGestureType: String?) -> Unit)? = null - var onTimeout: ((lastGestureType: String?) -> Unit)? = null + var onSteady: ((idleDurationMs: Double, lastGestureType: MapGestureType?) -> Unit)? = null + var onTimeout: ((lastGestureType: MapGestureType?) -> Unit)? = null private val handler = Handler(Looper.getMainLooper()) var activeAnimations: Int = 0 private set var isGestureActive: Boolean = false private set - private var lastGestureType: String? = null + private var lastGestureType: MapGestureType? = null var lastTransitionEndedAtMs: Double? = null private set private var quietRunnable: Runnable? = null @@ -71,24 +72,24 @@ class MapSteadyDetector( mapboxMap.gesturesPlugin { addOnMoveListener(object : OnMoveListener { - override fun onMoveBegin(detector: MoveGestureDetector) { handleGestureBegin("move") } + override fun onMoveBegin(detector: MoveGestureDetector) { handleGestureBegin(MapGestureType.Move) } override fun onMove(detector: MoveGestureDetector): Boolean = false - override fun onMoveEnd(detector: MoveGestureDetector) { handleGestureEnd("move") } + override fun onMoveEnd(detector: MoveGestureDetector) { handleGestureEnd(MapGestureType.Move) } }) addOnScaleListener(object : OnScaleListener { - override fun onScaleBegin(detector: StandardScaleGestureDetector) { handleGestureBegin("scale") } + override fun onScaleBegin(detector: StandardScaleGestureDetector) { handleGestureBegin(MapGestureType.Scale) } override fun onScale(detector: StandardScaleGestureDetector) {} - override fun onScaleEnd(detector: StandardScaleGestureDetector) { handleGestureEnd("scale") } + override fun onScaleEnd(detector: StandardScaleGestureDetector) { handleGestureEnd(MapGestureType.Scale) } }) addOnRotateListener(object : OnRotateListener { - override fun onRotateBegin(detector: RotateGestureDetector) { handleGestureBegin("rotate") } + override fun onRotateBegin(detector: RotateGestureDetector) { handleGestureBegin(MapGestureType.Rotate) } override fun onRotate(detector: RotateGestureDetector) {} - override fun onRotateEnd(detector: RotateGestureDetector) { handleGestureEnd("rotate") } + override fun onRotateEnd(detector: RotateGestureDetector) { handleGestureEnd(MapGestureType.Rotate) } }) addOnShoveListener(object : OnShoveListener { - override fun onShoveBegin(detector: ShoveGestureDetector) { handleGestureBegin("shove") } + override fun onShoveBegin(detector: ShoveGestureDetector) { handleGestureBegin(MapGestureType.Shove) } override fun onShove(detector: ShoveGestureDetector) {} - override fun onShoveEnd(detector: ShoveGestureDetector) { handleGestureEnd("shove") } + override fun onShoveEnd(detector: ShoveGestureDetector) { handleGestureEnd(MapGestureType.Shove) } }) } } @@ -132,7 +133,7 @@ class MapSteadyDetector( timeoutRunnable = runnable } - private fun markActivity(gestureType: String? = null) { + private fun markActivity(gestureType: MapGestureType? = null) { if (gestureType != null) lastGestureType = gestureType scheduleQuietCheck() scheduleTimeoutTimer() @@ -157,14 +158,14 @@ class MapSteadyDetector( scheduleQuietCheck() } - private fun handleGestureBegin(type: String) { + private fun handleGestureBegin(type: MapGestureType) { isGestureActive = true lastGestureType = type lastTransitionEndedAtMs = null markActivity(lastGestureType) } - private fun handleGestureEnd(type: String) { + private fun handleGestureEnd(type: MapGestureType) { lastGestureType = type isGestureActive = false lastTransitionEndedAtMs = nowMs() diff --git a/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt b/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt index 3d1384b1ee..1e6462064e 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/events/RNMBXCameraGestureObserver.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import com.rnmapbox.rnmbx.components.AbstractMapFeature import com.rnmapbox.rnmbx.components.RemovalReason +import com.rnmapbox.rnmbx.components.mapview.MapGestureType import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView import com.rnmapbox.rnmbx.components.mapview.helpers.MapSteadyDetector @@ -27,14 +28,13 @@ class RNMBXCameraGestureObserver( ) } - private fun normalizeGestureType(type: String?): String? { - return when (type) { - "move" -> "pan" - "scale" -> "pinch" - "rotate" -> "rotate" - "shove" -> "pitch" - else -> type - } + private fun normalizeGestureType(type: MapGestureType?): String? = when (type) { + MapGestureType.Move -> "pan" + MapGestureType.Scale -> "pinch" + MapGestureType.Rotate -> "rotate" + MapGestureType.Shove -> "pitch" + MapGestureType.Fling -> "fling" + null -> null } override fun addToMap(mapView: RNMBXMapView) {