Skip to content
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
package com.rnmapbox.rnmbx.components.annotation

import android.content.Context
import android.view.MotionEvent
import android.view.View.MeasureSpec
import android.view.ViewGroup
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.views.view.ReactViewGroup
import com.rnmapbox.rnmbx.components.camera.BaseEvent

class RNMBXMarkerViewContent(context: Context): ReactViewGroup(context) {
var inAdd: Boolean = false

// Track last reported translation to avoid feedback loop:
// Mapbox sets setTranslationX(512) → we fire event → JS sets transform:[{translateX:512}]
// → Fabric calls setTranslationX(512) again → same value → no re-fire.
private var lastReportedTx = Float.NaN
private var lastReportedTy = Float.NaN

init {
allowRenderingOutside()
}
Expand All @@ -17,6 +28,50 @@ class RNMBXMarkerViewContent(context: Context): ReactViewGroup(context) {
configureParentClipping()
}

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// On ACTION_DOWN, tell the parent MapView not to intercept subsequent MOVE/UP
// events for pan/zoom recognition — that would send CANCEL to child Pressables
// and suppress onPress. Android resets the disallow flag on each new DOWN, so
// calling this once per gesture is sufficient. See maplibre-react-native#1289.
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
parent?.requestDisallowInterceptTouchEvent(true)
}
return super.dispatchTouchEvent(ev)
}

override fun setTranslationX(translationX: Float) {
super.setTranslationX(translationX)
maybeFireAnnotationPositionEvent()
}

override fun setTranslationY(translationY: Float) {
super.setTranslationY(translationY)
maybeFireAnnotationPositionEvent()
}

private fun maybeFireAnnotationPositionEvent() {
val tx = translationX
val ty = translationY
// Dedup: skip if value unchanged (prevents feedback loop when Fabric
// re-applies the same transform prop back to setTranslationX/Y).
if (tx == lastReportedTx && ty == lastReportedTy) return
lastReportedTx = tx
lastReportedTy = ty

val reactContext = context as? ReactContext ?: return
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return
// Use getSurfaceId(view) — more reliable for Fabric than getSurfaceId(context)
val surfaceId = UIManagerHelper.getSurfaceId(this)
dispatcher.dispatchEvent(
BaseEvent(surfaceId, id, "topAnnotationPosition",
Arguments.createMap().apply {
putDouble("x", tx.toDouble())
putDouble("y", ty.toDouble())
},
canCoalesce = true)
)
}

private fun configureParentClipping() {
val parent = parent
if (parent is android.view.ViewGroup) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package com.rnmapbox.rnmbx.components.annotation

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.viewmanagers.RNMBXMarkerViewContentManagerInterface
import com.rnmapbox.rnmbx.components.AbstractEventEmitter


class RNMBXMarkerViewContentManager(reactApplicationContext: ReactApplicationContext) :
ViewGroupManager<RNMBXMarkerViewContent>(),
AbstractEventEmitter<RNMBXMarkerViewContent>(reactApplicationContext),
RNMBXMarkerViewContentManagerInterface<RNMBXMarkerView> {

override fun getName(): String {
return REACT_CLASS
}
Expand All @@ -17,6 +18,10 @@ class RNMBXMarkerViewContentManager(reactApplicationContext: ReactApplicationCon
return RNMBXMarkerViewContent(context)
}

override fun customEvents(): Map<String, String> {
return mapOf("topAnnotationPosition" to "onAnnotationPosition")
}

companion object {
const val REACT_CLASS = "RNMBXMarkerViewContent"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ class RNMBXMarkerViewManager(reactApplicationContext: ReactApplicationContext) :
}
}
}


})
}
}
Expand Down
8 changes: 5 additions & 3 deletions docs/MarkerView.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ component for a maximum of around 100 views displayed at one time.
This is implemented with view annotations on [Android](https://docs.mapbox.com/android/maps/guides/annotations/view-annotations/)
and [iOS](https://docs.mapbox.com/ios/maps/guides/annotations/view-annotations).

This component has no dedicated `onPress` method. Instead, you should handle gestures
with the React views passed in as `children`.
This component has no dedicated `onPress` method. Instead, handle gestures
with the React views passed in as `children` — Pressable, TouchableOpacity,
etc. all work including their visual feedback (opacity, scale, etc.).

## props

Expand Down Expand Up @@ -85,7 +86,8 @@ FIX ME NO DESCRIPTION
ReactReactElement
```
_required_
One or more valid React Native views.
One or more valid React Native views. You can use Pressable, TouchableOpacity,
etc. directly as children — onPress and touch feedback work correctly.



Expand Down
4 changes: 2 additions & 2 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5665,7 +5665,7 @@
"name": "MapView"
},
"MarkerView": {
"description": "MarkerView represents an interactive React Native marker on the map.\n\nIf you have static views, consider using PointAnnotation or SymbolLayer to display\nan image, as they'll offer much better performance. Mapbox suggests using this\ncomponent for a maximum of around 100 views displayed at one time.\n\nThis is implemented with view annotations on [Android](https://docs.mapbox.com/android/maps/guides/annotations/view-annotations/)\nand [iOS](https://docs.mapbox.com/ios/maps/guides/annotations/view-annotations).\n\nThis component has no dedicated `onPress` method. Instead, you should handle gestures\nwith the React views passed in as `children`.",
"description": "MarkerView represents an interactive React Native marker on the map.\n\nIf you have static views, consider using PointAnnotation or SymbolLayer to display\nan image, as they'll offer much better performance. Mapbox suggests using this\ncomponent for a maximum of around 100 views displayed at one time.\n\nThis is implemented with view annotations on [Android](https://docs.mapbox.com/android/maps/guides/annotations/view-annotations/)\nand [iOS](https://docs.mapbox.com/ios/maps/guides/annotations/view-annotations).\n\nThis component has no dedicated `onPress` method. Instead, handle gestures\nwith the React views passed in as `children` — Pressable, TouchableOpacity,\netc. all work including their visual feedback (opacity, scale, etc.).",
"displayName": "MarkerView",
"methods": [],
"props": [
Expand Down Expand Up @@ -5727,7 +5727,7 @@
"required": true,
"type": "ReactReactElement",
"default": "none",
"description": "One or more valid React Native views."
"description": "One or more valid React Native views. You can use Pressable, TouchableOpacity,\netc. directly as children — onPress and touch feedback work correctly."
}
],
"fileNameWithExt": "MarkerView.tsx",
Expand Down
6 changes: 4 additions & 2 deletions docs/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,11 @@
"title": "Marker View",
"tags": [
"PointAnnotation",
"MarkerView"
"MarkerView",
"Slider",
"Interactive"
],
"docs": "\nShows marker view and point annotations\n"
"docs": "\nShows marker view and point annotations, including an interactive marker with\nsliders, switch, counter, text input, and pressable button to verify complex\ntouch interactions inside a MarkerView.\n"
},
"fullPath": "example/src/examples/Annotations/MarkerView.tsx",
"relPath": "Annotations/MarkerView.tsx",
Expand Down
Loading
Loading