Skip to content

Commit be306a1

Browse files
pjleonard37github-actions[bot]
authored andcommitted
Add Appearances examples for iOS and Android [MAPSIOS-1994][MAPSAND-2367] (#7521)
Adds Appearances example implementations for both iOS and Android, demonstrating how to use feature-state to dynamically change symbol icons based on user interaction. The examples were adapted from the GL JS appearances example to maintain consistency across platforms. GitOrigin-RevId: 60cbaf554bdb8637d0d44a100a5220ad16b8ac45
1 parent 714f77f commit be306a1

File tree

7 files changed

+219
-0
lines changed

7 files changed

+219
-0
lines changed

compose-app/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,16 @@
317317
android:name="@string/category"
318318
android:value="@string/category_styles" />
319319
</activity>
320+
<activity
321+
android:name=".examples.style.AppearancesActivity"
322+
android:description="@string/description_appearances"
323+
android:exported="true"
324+
android:label="@string/activity_appearances"
325+
android:parentActivityName=".ExampleOverviewActivity">
326+
<meta-data
327+
android:name="@string/category"
328+
android:value="@string/category_styles" />
329+
</activity>
320330
<activity
321331
android:name=".examples.style.PrecipitationsActivity"
322332
android:description="@string/description_precipitations"
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package com.mapbox.maps.compose.testapp.examples.style
2+
3+
import android.graphics.BitmapFactory
4+
import android.os.Bundle
5+
import android.util.Log
6+
import androidx.activity.ComponentActivity
7+
import androidx.activity.compose.setContent
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableStateOf
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.Modifier
14+
import com.mapbox.bindgen.Value
15+
import com.mapbox.geojson.Point
16+
import com.mapbox.maps.LayerPosition
17+
import com.mapbox.maps.compose.testapp.ExampleScaffold
18+
import com.mapbox.maps.compose.testapp.R
19+
import com.mapbox.maps.compose.testapp.ui.theme.MapboxMapComposeTheme
20+
import com.mapbox.maps.extension.compose.MapEffect
21+
import com.mapbox.maps.extension.compose.MapboxMap
22+
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
23+
import com.mapbox.maps.extension.compose.rememberMapState
24+
import com.mapbox.maps.extension.compose.style.standard.MapboxStandardStyle
25+
import com.mapbox.maps.extension.compose.style.standard.rememberStandardStyleState
26+
import com.mapbox.maps.interactions.FeatureState
27+
import com.mapbox.maps.interactions.FeaturesetFeature
28+
29+
/**
30+
* Example demonstrating the experimental Appearances API for dynamic icon states.
31+
* Shows how to use appearances with feature-state to change icon images based on user interaction.
32+
* - Default: hotel icon
33+
* - Currently Selected: hotel-active icon
34+
* - Previously Clicked: hotel-clicked icon
35+
*/
36+
public class AppearancesActivity : ComponentActivity() {
37+
38+
override fun onCreate(savedInstanceState: Bundle?) {
39+
super.onCreate(savedInstanceState)
40+
41+
val hotelBitmap = BitmapFactory.decodeResource(resources, R.drawable.hotel)
42+
val hotelActiveBitmap = BitmapFactory.decodeResource(resources, R.drawable.hotel_active)
43+
val hotelClickedBitmap = BitmapFactory.decodeResource(resources, R.drawable.hotel_clicked)
44+
setContent {
45+
var selectedFeature by remember { mutableStateOf<FeaturesetFeature<FeatureState>?>(null) }
46+
val clickedFeatures = remember { mutableSetOf<FeaturesetFeature<FeatureState>>() }
47+
48+
MapboxMapComposeTheme {
49+
ExampleScaffold {
50+
MapboxMap(
51+
modifier = Modifier.fillMaxSize(),
52+
mapViewportState = rememberMapViewportState {
53+
setCameraOptions {
54+
center(Point.fromLngLat(1.8447281852, 42.10025506))
55+
zoom(15.5)
56+
pitch(0.0)
57+
bearing(0.0)
58+
}
59+
},
60+
mapState = rememberMapState(),
61+
style = {
62+
MapboxStandardStyle(
63+
standardStyleState = rememberStandardStyleState {
64+
// When a hotel icon is clicked, set the currentlySelected feature state to true,
65+
// unselect the previous one if any, and store this feature both as the selected
66+
// feature and in the list of features that have been clicked
67+
interactionsState.onLayerClicked("points") { feature, _ ->
68+
// Clear the currently selected feature by resetting its feature state
69+
selectedFeature?.setFeatureState(
70+
FeatureState { addBooleanState(CURRENTLY_SELECTED_KEY, false) }
71+
) {}
72+
73+
// Store this feature as the currently selected feature and in the list
74+
// of features that have been clicked
75+
clickedFeatures.add(feature)
76+
feature.setFeatureState(
77+
FeatureState {
78+
addBooleanState(CURRENTLY_SELECTED_KEY, true)
79+
addBooleanState(HAS_BEEN_CLICKED_KEY, true)
80+
}
81+
) {}
82+
selectedFeature = feature
83+
true
84+
}
85+
86+
// When the map is clicked outside of any feature, unselect the currently selected
87+
// feature if there's any, or remove all features from the list of features that
88+
// have been clicked to get back to the initial state
89+
interactionsState.onMapClicked {
90+
if (selectedFeature != null) {
91+
// Unselect the currently selected feature
92+
selectedFeature?.setFeatureState(
93+
FeatureState { addBooleanState(CURRENTLY_SELECTED_KEY, false) }
94+
) {}
95+
selectedFeature = null
96+
} else {
97+
// Reset the state of all features to the default one
98+
clickedFeatures.forEach { clickedFeature ->
99+
clickedFeature.setFeatureState(
100+
FeatureState { addBooleanState(HAS_BEEN_CLICKED_KEY, false) }
101+
) {}
102+
}
103+
clickedFeatures.clear()
104+
}
105+
true
106+
}
107+
}
108+
)
109+
}
110+
) {
111+
// Add images, source, and layer after the style has loaded
112+
MapEffect(Unit) { mapView ->
113+
mapView.mapboxMap.subscribeStyleLoaded {
114+
mapView.mapboxMap.style?.let { style ->
115+
116+
// Load an image for every feature state
117+
style.addImage("hotel", hotelBitmap)
118+
style.addImage("hotel-active", hotelActiveBitmap)
119+
style.addImage("hotel-clicked", hotelClickedBitmap)
120+
121+
// Add a GeoJSON source with hotel locations
122+
style.addStyleSource(
123+
"points",
124+
Value.fromJson(
125+
"""
126+
{
127+
"type": "geojson",
128+
"data": $HOTEL_GEOJSON
129+
}
130+
""".trimIndent()
131+
).value!!
132+
)
133+
134+
// Add a layer to show an icon on every point with appearances
135+
// - When currentlySelected feature state is true: use "hotel-active" icon
136+
// - When hasBeenClicked feature state is true and currentlySelected is not: use "hotel-clicked" icon
137+
// - Otherwise: use the default "hotel" icon defined in layout
138+
// Appearances are experimental and subject to change in future versions
139+
try {
140+
style.addStyleLayer(
141+
Value.fromJson(POINTS_LAYER_JSON).value!!,
142+
LayerPosition(null, null, null)
143+
)
144+
} catch (e: Exception) {
145+
Log.e("Appearances", "Error adding layer", e)
146+
}
147+
}
148+
}
149+
}
150+
}
151+
}
152+
}
153+
}
154+
}
155+
156+
private companion object {
157+
private const val CURRENTLY_SELECTED_KEY = "currentlySelected"
158+
private const val HAS_BEEN_CLICKED_KEY = "hasBeenClicked"
159+
}
160+
}
161+
162+
private const val POINTS_LAYER_JSON = """
163+
{
164+
"id": "points",
165+
"type": "symbol",
166+
"source": "points",
167+
"layout": {
168+
"icon-allow-overlap": true,
169+
"icon-image": "hotel",
170+
"icon-size": 1.0,
171+
"icon-anchor": "center"
172+
},
173+
"appearances": [
174+
{
175+
"name": "currently-selected",
176+
"condition": ["boolean", ["feature-state", "currentlySelected"], false],
177+
"properties": {
178+
"icon-image": "hotel-active"
179+
}
180+
},
181+
{
182+
"name": "has-been-clicked",
183+
"condition": ["boolean", ["feature-state", "hasBeenClicked"], false],
184+
"properties": {
185+
"icon-image": "hotel-clicked"
186+
}
187+
}
188+
]
189+
}
190+
"""
191+
192+
private const val HOTEL_GEOJSON = """
193+
{
194+
"type": "FeatureCollection",
195+
"features": [
196+
{"type": "Feature", "id": "1", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8452993238082342, 42.100164223399275]}},
197+
{"type": "Feature", "id": "2", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8438590191857145, 42.1004178052402]}},
198+
{"type": "Feature", "id": "3", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.844225198327564, 42.10130533369667]}},
199+
{"type": "Feature", "id": "4", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8443594640122, 42.0990955459275]}},
200+
{"type": "Feature", "id": "5", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8449697625811154, 42.09869705141318]}},
201+
{"type": "Feature", "id": "6", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8471058075726603, 42.09978384873651]}},
202+
{"type": "Feature", "id": "7", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8455739474818813, 42.10182152060625]}},
203+
{"type": "Feature", "id": "8", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8427787800360136, 42.10039061289771]}},
204+
{"type": "Feature", "id": "9", "properties": {}, "geometry": {"type": "Point", "coordinates": [1.8433280487479635, 42.0994396753579]}}
205+
]
206+
}
207+
"""
1.45 KB
Loading
2.83 KB
Loading
1.95 KB
Loading

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<string name="description_standard_style">Showcase usage of Standard style</string>
2929
<string name="description_clip_layer">Showcase the usage of clip layer.</string>
3030
<string name="description_interactions">Showcase the interactions.</string>
31+
<string name="description_appearances">Change icon images dynamically using the Appearances API with feature-state</string>
3132
<string name="description_precipitations">Showcase the rain and snow effects.</string>
3233
<string name="description_color_theme">Showcase color theme.</string>
3334
<string name="description_elevated_line">Showcase line elevation</string>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<string name="activity_standard_style">Standard style</string>
2929
<string name="activity_clip_layer">Clip layer example</string>
3030
<string name="activity_interactions">Interactions example</string>
31+
<string name="activity_appearances">Appearances</string>
3132
<string name="activity_precipitations">Precipitations example</string>
3233
<string name="activity_color_theme">Color theme example</string>
3334
<string name="activity_elevated_line">Elevated line example</string>

0 commit comments

Comments
 (0)