diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js index d7e7299680..45b6ed7cee 100644 --- a/__tests__/interface.test.js +++ b/__tests__/interface.test.js @@ -9,6 +9,7 @@ describe('Public Interface', () => { 'StyleSheet', 'Light', 'PointAnnotation', + 'PointAnnotationManager', 'MarkerView', 'Annotation', 'Callout', diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index f82fadd32d..0b9bf05813 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -11,6 +11,7 @@ import com.rnmapbox.rnmbx.components.annotation.RNMBXCalloutManager import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewContentManager import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewManager import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManager +import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManagerViewManager import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationModule import com.rnmapbox.rnmbx.components.camera.RNMBXCameraManager import com.rnmapbox.rnmbx.components.camera.RNMBXCameraModule @@ -135,6 +136,7 @@ class RNMBXPackage : TurboReactPackage() { managers.add(RNMBXMarkerViewManager(reactApplicationContext)) managers.add(RNMBXMarkerViewContentManager(reactApplicationContext)) managers.add(RNMBXPointAnnotationManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXPointAnnotationManager"))) + managers.add(RNMBXPointAnnotationManagerViewManager(reactApplicationContext)) managers.add(RNMBXCalloutManager()) managers.add(RNMBXNativeUserLocationManager()) managers.add(RNMBXCustomLocationProviderManager()) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt new file mode 100644 index 0000000000..ba21f03ece --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt @@ -0,0 +1,30 @@ +package com.rnmapbox.rnmbx.components.annotation + +import android.content.Context +import com.rnmapbox.rnmbx.components.AbstractMapFeature +import com.rnmapbox.rnmbx.components.RemovalReason +import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView + +class RNMBXPointAnnotationManagerView(context: Context) : AbstractMapFeature(context) { + var slot: String? = null + set(value) { + field = value + applySlot() + } + + private fun applySlot() { + withMapView { mapView -> + mapView.pointAnnotations?.manager?.slot = slot + } + } + + override fun addToMap(mapView: RNMBXMapView) { + super.addToMap(mapView) + applySlot() + } + + override fun removeFromMap(mapView: RNMBXMapView, reason: RemovalReason): Boolean { + mapView.pointAnnotations?.manager?.slot = null + return super.removeFromMap(mapView, reason) + } +} diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt new file mode 100644 index 0000000000..0c3441114e --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt @@ -0,0 +1,34 @@ +package com.rnmapbox.rnmbx.components.annotation + +import com.facebook.react.bridge.Dynamic +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.common.MapBuilder +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.RNMBXPointAnnotationManagerManagerInterface +import com.rnmapbox.rnmbx.components.AbstractEventEmitter + +class RNMBXPointAnnotationManagerViewManager(context: ReactApplicationContext) : + AbstractEventEmitter(context), + RNMBXPointAnnotationManagerManagerInterface { + override fun customEvents(): Map? { + return MapBuilder.builder().build() + } + + override fun getName(): String { + return REACT_CLASS + } + + override fun createViewInstance(context: ThemedReactContext): RNMBXPointAnnotationManagerView { + return RNMBXPointAnnotationManagerView(context) + } + + companion object { + const val REACT_CLASS = "RNMBXPointAnnotationManager" + } + + @ReactProp(name = "slot") + override fun setSlot(view: RNMBXPointAnnotationManagerView, value: Dynamic) { + view.slot = if (value.isNull) null else value.asString() + } +} diff --git a/docs/docs.json b/docs/docs.json index 399a40f854..fe3b9112d3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -6529,6 +6529,30 @@ "relPath": "src/components/PointAnnotation.tsx", "name": "PointAnnotation" }, + "PointAnnotationManager": { + "description": "Configures the shared PointAnnotation manager for the parent MapView.\nWrap PointAnnotation components as children.", + "displayName": "PointAnnotationManager", + "methods": [], + "props": [ + { + "name": "slot", + "required": false, + "type": "Slot \\| (string & {})", + "default": "none", + "description": "The slot in the style layer stack to position the annotation layer.\nUse with Mapbox Standard style to control layer ordering." + }, + { + "name": "children", + "required": false, + "type": "ReactNode", + "default": "none", + "description": "FIX ME NO DESCRIPTION" + } + ], + "fileNameWithExt": "PointAnnotationManager.tsx", + "relPath": "src/components/PointAnnotationManager.tsx", + "name": "PointAnnotationManager" + }, "Rain": { "description": "", "displayName": "Rain", diff --git a/docs/examples.json b/docs/examples.json index 6567edf548..64ea144bbe 100644 --- a/docs/examples.json +++ b/docs/examples.json @@ -672,6 +672,20 @@ "relPath": "Annotations/PointAnnotationAnchors.js", "name": "PointAnnotationAnchors" }, + { + "metadata": { + "title": "PointAnnotationManager Slot", + "tags": [ + "PointAnnotationManager", + "PointAnnotation", + "slot" + ], + "docs": "\nDemonstrates using PointAnnotationManager to position annotations\nin different slots of the Mapbox Standard style.\n " + }, + "fullPath": "example/src/examples/Annotations/PointAnnotationManagerSlot.tsx", + "relPath": "Annotations/PointAnnotationManagerSlot.tsx", + "name": "PointAnnotationManagerSlot" + }, { "metadata": { "title": "Show Point Annotations", diff --git a/example/src/examples/Annotations/MarkerViewDisappear.tsx b/example/src/examples/Annotations/MarkerViewDisappear.tsx new file mode 100644 index 0000000000..023a71c096 --- /dev/null +++ b/example/src/examples/Annotations/MarkerViewDisappear.tsx @@ -0,0 +1,89 @@ +import { Camera, MapView, MarkerView } from '@rnmapbox/maps'; +import { useEffect, useState } from 'react'; +import { Platform, Pressable, StyleSheet, Text, View } from 'react-native'; + +const POSITIONS = Object.fromEntries([ + ['id1', [-1.202582, 43.36005] as [number, number]], + ['id2', [-1.303701, 43.384357] as [number, number]], +]); + +type Rider = { + id: string; + name: string; + order: number; +}; + +const RIDERS: Rider[] = [ + { id: 'id2', name: 'Favorite Rider', order: 100 }, + { id: 'id1', name: 'Normal Rider', order: 10 }, +]; + +export const Test = () => { + const [selectedId, setSelectedId] = useState(null); + const [positions, setPositions] = useState>({}); + + useEffect(() => { + const interval = setInterval(() => { + setPositions(POSITIONS); + }, 1000); + return () => clearInterval(interval); + }, []); + + const labels = RIDERS.map(rider => ({ + ...rider, + order: rider.id === selectedId ? 10000 : rider.order, + selected: rider.id === selectedId, + })).sort((a, b) => a.order - b.order); + + return ( + + { + if (Platform.OS !== 'web') setSelectedId(null); + }}> + + + {labels.map(label => { + const position = positions[label.id]; + if (!position) return null; + + return ( + + setSelectedId(label.id)}> + + + {label.name} + + + + + ); + })} + + + ); +}; + +export default Test; + +/** @type ExampleWithMetadata['metadata'] */ +const metadata = { + title: 'MarkerView Disappear (Issue 4206)', + tags: ['MarkerView', 'bug', 'isSelected'], + docs: 'Exact reproducer from issue #4206: MarkerViews disappear on Android when width/height is 0 during addViewAnnotation.', +}; +Test.metadata = metadata; diff --git a/example/src/examples/Annotations/PointAnnotationManagerSlot.tsx b/example/src/examples/Annotations/PointAnnotationManagerSlot.tsx new file mode 100644 index 0000000000..bb7423589c --- /dev/null +++ b/example/src/examples/Annotations/PointAnnotationManagerSlot.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { + Camera, + MapView, + PointAnnotation, + PointAnnotationManager, +} from '@rnmapbox/maps'; +import { Button } from '@rneui/base'; + +import type { ExampleWithMetadata } from '../common/ExampleMetadata'; // exclude-from-doc + +const styles = StyleSheet.create({ + map: { flex: 1 }, + pin: { + width: 30, + height: 30, + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + }, + label: { color: 'white', fontWeight: 'bold', fontSize: 12 }, + buttons: { + flexDirection: 'row', + justifyContent: 'center', + padding: 8, + gap: 8, + }, +}); + +const COORDS: [number, number][] = [ + [-74.00597, 40.71427], + [-74.0065, 40.7128], + [-74.0045, 40.7155], +]; + +const PointAnnotationManagerSlot = () => { + const [slot, setSlot] = useState('middle'); + + return ( + <> + + + + {COORDS.map((coord, i) => ( + + + {i + 1} + + + ))} + + + +