Skip to content

Commit 0b39d2f

Browse files
pjleonard37Release SDK bot for Maps SDK teamkediarov
authored andcommitted
Add Default Markers to iOS and Android Maps SDKs (#3597)
JIRA epic [here](https://mapbox.atlassian.net/browse/MAPSSDK-813). This PR adds default markers to our iOS and Android Maps SDKs. These Markers are convenience implementations of View Annotations with a limited set of customization options (color, stoke, inner color, text). Markers are only available in Swift UI and JetPack Compose. --------- Co-authored-by: Release SDK bot for Maps SDK team <maps_sdk_ios@mapbox.com> Co-authored-by: Kirill Kediarov <kirill.kediarov@mapbox.com> GitOrigin-RevId: c6793cba5540e00cf049861962fa9deab1781630
1 parent 8b0c2b3 commit 0b39d2f

File tree

14 files changed

+578
-0
lines changed

14 files changed

+578
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Mapbox welcomes participation and contributions from everyone.
2121
* Added `setContentDescription()` method to `AttributionPlugin` and `AttributionView` interfaces to programmatically set accessibility content description for the attribution button.
2222
* Added `MapView.onResume()` which should be called in `onResume()` of the host activity or fragment to resume the map view if `plugin-lifecycle` is not used.
2323
* Improved zoom animation performance by preloading target tiles and reducing unnecessary intermediate tile processing, resulting in smoother camera transitions and reduced frame rate drops.
24+
* Introduce experimental `Marker` convenience API in Jetpack Compose. Use `Marker` to quickly add a `ViewAnnotation` pin at the specified coordinates with custom text, color, and stroke.
2425

2526
## Bug fixes 🐞
2627
* Fix exception when accessing enum properties in annotations.

compose-app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@
4343
android:name="@string/category"
4444
android:value="@string/category_basic" />
4545
</activity>
46+
<activity
47+
android:name=".examples.annotation.MarkersActivity"
48+
android:configChanges="orientation|screenSize|screenLayout"
49+
android:description="@string/description_markers"
50+
android:exported="true"
51+
android:label="@string/activity_markers"
52+
android:parentActivityName=".ExampleOverviewActivity">
53+
<meta-data
54+
android:name="@string/category"
55+
android:value="@string/category_annotation" />
56+
</activity>
4657
<activity
4758
android:name=".examples.annotation.CircleAnnotationActivity"
4859
android:configChanges="orientation|screenSize|screenLayout"
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package com.mapbox.maps.compose.testapp.examples.annotation
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.compose.foundation.background
7+
import androidx.compose.foundation.border
8+
import androidx.compose.foundation.clickable
9+
import androidx.compose.foundation.layout.Arrangement
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Row
13+
import androidx.compose.foundation.layout.fillMaxSize
14+
import androidx.compose.foundation.layout.fillMaxWidth
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.layout.widthIn
18+
import androidx.compose.foundation.shape.RoundedCornerShape
19+
import androidx.compose.material.FloatingActionButton
20+
import androidx.compose.material.Text
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.getValue
23+
import androidx.compose.runtime.mutableStateListOf
24+
import androidx.compose.runtime.mutableStateOf
25+
import androidx.compose.runtime.remember
26+
import androidx.compose.runtime.setValue
27+
import androidx.compose.ui.Alignment
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.graphics.Color
30+
import androidx.compose.ui.unit.dp
31+
import com.mapbox.geojson.Point
32+
import com.mapbox.maps.MapboxExperimental
33+
import com.mapbox.maps.compose.testapp.ExampleScaffold
34+
import com.mapbox.maps.compose.testapp.examples.utils.CityLocations.HELSINKI
35+
import com.mapbox.maps.compose.testapp.ui.theme.MapboxMapComposeTheme
36+
import com.mapbox.maps.extension.compose.MapboxMap
37+
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
38+
import com.mapbox.maps.extension.compose.annotation.Marker
39+
import com.mapbox.maps.extension.compose.style.standard.MapboxStandardStyle
40+
import com.mapbox.maps.extension.compose.style.standard.rememberStandardStyleState
41+
42+
/**
43+
* Example to showcase usage of Markers.
44+
*/
45+
public class MarkersActivity : ComponentActivity() {
46+
@OptIn(MapboxExperimental::class)
47+
override fun onCreate(savedInstanceState: Bundle?) {
48+
super.onCreate(savedInstanceState)
49+
setContent {
50+
val tappedPoints = remember { mutableStateListOf<Point>() }
51+
var markerColor by remember { mutableStateOf(Color(0xffcfdaf7)) }
52+
var strokeColor by remember { mutableStateOf(Color(0xff3a59fa)) }
53+
var showStroke by remember { mutableStateOf(true) }
54+
var showText by remember { mutableStateOf(true) }
55+
56+
MapboxMapComposeTheme {
57+
ExampleScaffold(
58+
floatingActionButton = {
59+
SelectionBox(
60+
markerColor = markerColor,
61+
onMarkerColorChange = { markerColor = it },
62+
strokeColor = strokeColor,
63+
onStrokeColorChange = { strokeColor = it },
64+
showText = showText,
65+
onShowTextToggle = { showText = !showText },
66+
showStroke = showStroke,
67+
onShowStrokeToggle = { showStroke = !showStroke }
68+
)
69+
}
70+
) {
71+
MapboxMap(
72+
Modifier.fillMaxSize(),
73+
mapViewportState = rememberMapViewportState {
74+
setCameraOptions {
75+
zoom(ZOOM)
76+
pitch(PITCH)
77+
center(HELSINKI)
78+
}
79+
},
80+
style = {
81+
MapboxStandardStyle(
82+
standardStyleState = rememberStandardStyleState {
83+
interactionsState.onMapClicked { context ->
84+
tappedPoints.add(context.coordinateInfo.coordinate)
85+
return@onMapClicked true
86+
}
87+
interactionsState.onMapLongClicked { _ ->
88+
tappedPoints.clear()
89+
return@onMapLongClicked true
90+
}
91+
}
92+
)
93+
}
94+
) {
95+
Marker(
96+
point = HELSINKI,
97+
color = markerColor,
98+
stroke = if (showStroke) strokeColor else null,
99+
text = if (showText) "Central Helsinki" else null
100+
)
101+
tappedPoints.forEach { it ->
102+
Marker(
103+
point = it,
104+
color = markerColor,
105+
stroke = if (showStroke) strokeColor else null,
106+
text = if (showText) String.format("%.3f, %.3f", it.latitude(), it.longitude()) else null
107+
)
108+
}
109+
}
110+
}
111+
}
112+
}
113+
}
114+
115+
private companion object {
116+
const val ZOOM: Double = 16.0
117+
const val PITCH: Double = 60.0
118+
}
119+
}
120+
121+
// Add box to customize the markers
122+
@Composable
123+
private fun SelectionBox(
124+
markerColor: Color,
125+
onMarkerColorChange: (Color) -> Unit,
126+
strokeColor: Color,
127+
onStrokeColorChange: (Color) -> Unit,
128+
showText: Boolean,
129+
onShowTextToggle: () -> Unit,
130+
showStroke: Boolean,
131+
onShowStrokeToggle: () -> Unit
132+
) {
133+
Box(
134+
modifier = Modifier
135+
.fillMaxWidth()
136+
.padding(bottom = 15.dp),
137+
contentAlignment = Alignment.BottomCenter
138+
) {
139+
Column(
140+
modifier = Modifier
141+
.widthIn(max = 340.dp)
142+
.background(Color(0xCC222222), shape = RoundedCornerShape(16.dp))
143+
.padding(10.dp),
144+
horizontalAlignment = Alignment.CenterHorizontally
145+
) {
146+
Text(
147+
text = "Tap map to add a Marker",
148+
color = Color.White,
149+
modifier = Modifier.padding(bottom = 4.dp)
150+
)
151+
152+
// Marker Color Selector
153+
Row(
154+
verticalAlignment = Alignment.CenterVertically,
155+
modifier = Modifier.padding(top = 4.dp)
156+
) {
157+
Text(
158+
text = "Marker:",
159+
color = Color.White,
160+
modifier = Modifier.padding(end = 4.dp)
161+
)
162+
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
163+
colorOptions.forEach { color ->
164+
Box(
165+
modifier = Modifier
166+
.size(20.dp)
167+
.background(color, shape = RoundedCornerShape(10.dp))
168+
.clickable { onMarkerColorChange(color) }
169+
.border(
170+
width = if (markerColor == color) 2.dp else 0.dp,
171+
color = if (markerColor == color) Color.White else Color.Transparent,
172+
shape = RoundedCornerShape(10.dp)
173+
)
174+
)
175+
}
176+
}
177+
}
178+
179+
// Stroke Color Selector
180+
Row(
181+
verticalAlignment = Alignment.CenterVertically,
182+
modifier = Modifier.padding(top = 2.dp)
183+
) {
184+
Text(
185+
text = "Stroke:",
186+
color = Color.White,
187+
modifier = Modifier.padding(end = 4.dp)
188+
)
189+
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
190+
colorOptions.forEach { color ->
191+
Box(
192+
modifier = Modifier
193+
.size(20.dp)
194+
.background(color, shape = RoundedCornerShape(10.dp))
195+
.clickable { onStrokeColorChange(color) }
196+
.border(
197+
width = if (strokeColor == color) 2.dp else 0.dp,
198+
color = if (strokeColor == color) Color.White else Color.Transparent,
199+
shape = RoundedCornerShape(10.dp)
200+
)
201+
)
202+
}
203+
}
204+
}
205+
206+
// Toggle Buttons
207+
Row(
208+
horizontalArrangement = Arrangement.spacedBy(8.dp),
209+
modifier = Modifier.padding(top = 8.dp)
210+
) {
211+
FloatingActionButton(
212+
modifier = Modifier.size(width = 150.dp, height = 32.dp),
213+
backgroundColor = if (showText) Color(0xFF1976D2) else Color(0xFFB0BEC5),
214+
onClick = onShowTextToggle,
215+
shape = RoundedCornerShape(16.dp)
216+
) {
217+
Text(
218+
text = if (showText) "Hide text" else "Show text",
219+
color = Color.White
220+
)
221+
}
222+
FloatingActionButton(
223+
modifier = Modifier.size(width = 150.dp, height = 32.dp),
224+
backgroundColor = if (showStroke) Color(0xFF1976D2) else Color(0xFFB0BEC5),
225+
onClick = onShowStrokeToggle,
226+
shape = RoundedCornerShape(16.dp)
227+
) {
228+
Text(
229+
text = if (showStroke) "Hide stroke" else "Show stroke",
230+
color = Color.White
231+
)
232+
}
233+
}
234+
}
235+
}
236+
}
237+
238+
private val colorOptions = listOf(
239+
Color.Red, Color.Green, Color(0xff4264fb), Color.Yellow, Color.Magenta, Color.Cyan,
240+
Color.Black, Color.White, Color.Gray, Color(0xff0f38bf)
241+
)

compose-app/src/main/res/values/example_descriptions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<resources>
33
<string name="description_simple_map">Create and display a map that uses the default Mapbox streets style. This example also shows how to update the starting camera for a map.</string>
44
<string name="description_debug_mode">Use variations of debug modes of the Map</string>
5+
<string name="description_markers">Add Markers on the Map</string>
56
<string name="description_circle_annotation">Add CircleAnnotation on the Map</string>
67
<string name="description_point_annotation">Add PointAnnotation on the Map</string>
78
<string name="description_point_annotation_cluster">Add PointAnnotation cluster on the Map</string>

compose-app/src/main/res/values/example_titles.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<resources>
33
<string name="activity_simple_map">Display a map view</string>
44
<string name="activity_debug_mode">Debug mode</string>
5+
<string name="activity_markers">Markers</string>
56
<string name="activity_circle_annotation">Circle Annotation</string>
67
<string name="activity_point_annotation">Point Annotation</string>
78
<string name="activity_point_annotation_cluster">Point Annotation Clustering</string>

extension-compose/api/Release/metalava.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ package com.mapbox.maps.extension.compose.annotation {
172172
method @androidx.compose.runtime.Composable public static com.mapbox.maps.extension.compose.annotation.IconImage rememberIconImage(@DrawableRes int resourceId);
173173
}
174174

175+
public final class MarkerKt {
176+
method @androidx.compose.runtime.Composable @com.mapbox.maps.MapboxExperimental @com.mapbox.maps.extension.compose.MapboxMapComposable public static void Marker(com.mapbox.geojson.Point point, long color = Color(4291812087), long innerColor = Color(4294967295), androidx.compose.ui.graphics.Color? stroke = Color(4282014202), String? text = null);
177+
}
178+
175179
public final class ViewAnnotationKt {
176180
method @androidx.compose.runtime.Composable @com.mapbox.maps.extension.compose.MapboxMapComposable public static void ViewAnnotation(com.mapbox.maps.ViewAnnotationOptions options, androidx.compose.ui.Modifier modifier = Modifier, com.mapbox.maps.viewannotation.OnViewAnnotationUpdatedListener? onUpdatedListener = null, kotlin.jvm.functions.Function0<kotlin.Unit> content);
177181
}

extension-compose/api/extension-compose.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ public final class com/mapbox/maps/extension/compose/annotation/IconImageKt {
196196
public static final fun rememberIconImage (Ljava/lang/Object;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/runtime/Composer;I)Lcom/mapbox/maps/extension/compose/annotation/IconImage;
197197
}
198198

199+
public final class com/mapbox/maps/extension/compose/annotation/MarkerKt {
200+
public static final fun Marker-YiRMAXg (Lcom/mapbox/geojson/Point;JJLandroidx/compose/ui/graphics/Color;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V
201+
}
202+
199203
public final class com/mapbox/maps/extension/compose/annotation/ViewAnnotationKt {
200204
public static final fun ViewAnnotation (Lcom/mapbox/maps/ViewAnnotationOptions;Landroidx/compose/ui/Modifier;Lcom/mapbox/maps/viewannotation/OnViewAnnotationUpdatedListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
201205
}

extension-compose/src/androidTest/java/com/mapbox/maps/extension/compose/annotation/ViewAnnotationTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import androidx.compose.ui.test.onNodeWithText
1414
import androidx.compose.ui.test.performTouchInput
1515
import androidx.compose.ui.test.swipeLeft
1616
import com.mapbox.geojson.Point
17+
import com.mapbox.maps.MapboxExperimental
1718
import com.mapbox.maps.ViewAnnotationAnchor
19+
import com.mapbox.maps.extension.compose.MapEffect
1820
import com.mapbox.maps.extension.compose.MapboxMap
1921
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
2022
import com.mapbox.maps.extension.compose.internal.utils.CityLocations.HELSINKI
@@ -93,6 +95,49 @@ public class ViewAnnotationTest {
9395
composeTestRule.onNodeWithText(VIEW_ANNOTATION_TEXT).assertDoesNotExist()
9496
}
9597

98+
@OptIn(MapboxExperimental::class)
99+
@Test
100+
public fun testMarker() {
101+
var viewAnnotationCount = 0
102+
103+
composeTestRule.setContent {
104+
MapboxMap(
105+
Modifier
106+
.fillMaxSize()
107+
.testTag(MAP_TEST_TAG),
108+
mapViewportState = rememberMapViewportState {
109+
setCameraOptions {
110+
zoom(ZOOM)
111+
center(HELSINKI)
112+
}
113+
},
114+
mapState = rememberMapState(),
115+
) {
116+
Marker(
117+
point = HELSINKI,
118+
text = VIEW_ANNOTATION_TEXT
119+
)
120+
121+
// Use MapEffect to access ViewAnnotationManager and count annotations
122+
MapEffect(Unit) { mapView ->
123+
viewAnnotationCount = mapView.viewAnnotationManager.annotations.size
124+
}
125+
}
126+
}
127+
composeTestRule.waitForIdle()
128+
129+
// Test that the Marker actually adds a ViewAnnotation to the map
130+
composeTestRule.waitUntil(timeoutMillis = VIEW_APPEAR_TIMEOUT_MS, condition = {
131+
viewAnnotationCount > 0
132+
})
133+
134+
// Verify that exactly one view annotation was added by the Marker
135+
assert(viewAnnotationCount == 1) { "Expected 1 view annotation, got $viewAnnotationCount" }
136+
137+
// Verify the map node exists
138+
composeTestRule.onNodeWithTag(MAP_TEST_TAG).assertExists()
139+
}
140+
96141
private fun setMapContent(
97142
cameraCenter: Point,
98143
annotationCenter: Point,

0 commit comments

Comments
 (0)