1. Avant de commencer
Cet atelier de programmation vous explique comment intégrer le SDK Maps pour Android à votre application et utiliser ses principales fonctionnalités. Pour cela, nous allons créer une application affichant une carte des montagnes du Colorado, aux États-Unis, à l'aide de différents types de repères. Vous apprendrez également à dessiner d'autres formes sur la carte.
Voici à quoi cela ressemblera à la fin de l'atelier de programmation :
Prérequis
- Connaissances de base de Kotlin, de Jetpack Compose et du développement Android
Objectifs de l'atelier
- Activer et utiliser la bibliothèque Maps Compose pour le SDK Maps pour Android afin d'ajouter un
GoogleMap
à une application Android - Ajouter et personnaliser des repères
- Dessiner des polygones sur la carte
- Contrôler le point de vue de la caméra de manière programmatique
Prérequis
- SDK Maps pour Android
- Un compte Google pour lequel la facturation est activée
- La dernière version stable d'Android Studio
- Un appareil ou un émulateur Android qui exécute la plate-forme des API Google sous Android version 5.0 ou ultérieure (pour connaître la procédure d'installation, consultez Exécuter des applications sur l'émulateur Android).
- Une connexion Internet
2. Configuration
Pour l'étape suivante, vous devez activer le SDK Maps pour Android.
Configurer Google Maps Platform
Si vous ne disposez pas encore d'un compte Google Cloud Platform et d'un projet pour lequel la facturation est activée, consultez le guide Premiers pas avec Google Maps Platform pour savoir comment créer un compte de facturation et un projet.
- Dans Cloud Console, cliquez sur le menu déroulant des projets, puis sélectionnez celui que vous souhaitez utiliser pour cet atelier de programmation.
- Activez les API et les SDK Google Maps Platform requis pour cet atelier de programmation dans Google Cloud Marketplace. Pour ce faire, suivez les étapes indiquées dans cette vidéo ou dans cette documentation.
- Générez une clé API sur la page Identifiants de Cloud Console. Vous pouvez suivre la procédure décrite dans cette vidéo ou dans cette documentation. Toutes les requêtes envoyées à Google Maps Platform nécessitent une clé API.
3. Démarrage rapide
Voici un code de démarrage qui vous permettra de commencer rapidement cet atelier de programmation. Vous pouvez tout à fait passer directement au résultat, mais si vous souhaitez voir toutes les étapes et les réaliser de votre côté, poursuivez votre lecture.
- Clonez le dépôt si vous avez installé
git
.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Vous pouvez également cliquer sur le bouton suivant pour télécharger le code source.
- Une fois le code obtenu, dans Android Studio, ouvrez le projet qui se trouve dans le répertoire
starter
.
4. Ajoutez votre clé API au projet
Cette section explique comment stocker votre clé API pour qu'elle puisse être référencée de manière sécurisée par votre application. Vous ne devez pas enregistrer votre clé API dans votre système de contrôle des versions. Nous vous recommandons donc de la stocker dans le fichier secrets.properties
, qui sera placé dans votre copie locale du répertoire racine de votre projet. Pour en savoir plus sur le fichier secrets.properties
, consultez Fichiers de propriétés Gradle.
Pour vous faciliter la tâche, nous vous recommandons d'utiliser le plug-in Secrets Gradle pour Android.
Pour installer le plug-in Secrets Gradle pour Android dans votre projet Google Maps :
- Dans Android Studio, ouvrez votre fichier
build.gradle.kts
de premier niveau et ajoutez le code suivant à l'élémentdependencies
sousbuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Ouvrez le fichier
build.gradle.kts
au niveau du module et ajoutez le code suivant à l'élémentplugins
.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- Dans le fichier
build.gradle.kts
au niveau du module, assurez-vous quetargetSdk
etcompileSdk
sont définis sur au moins 34. - Enregistrez le fichier et synchronisez votre projet avec Gradle.
- Ouvrez le fichier
secrets.properties
dans votre répertoire de premier niveau, puis ajoutez le code suivant. RemplacezYOUR_API_KEY
par votre clé API. Stockez votre clé dans ce fichier, carsecrets.properties
n'est pas vérifié dans un système de contrôle des versions.MAPS_API_KEY=YOUR_API_KEY
- Enregistrez le fichier.
- Créez le fichier
local.defaults.properties
dans votre répertoire de premier niveau (même dossier que le fichiersecrets.properties
), puis ajoutez le code suivant. Ce fichier a pour but de fournir un emplacement de sauvegarde de la clé API, à utiliser si le fichierMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
est introuvable pour éviter l'échec des créations. Cette situation se produit lorsque vous clonez l'application à partir d'un système de contrôle des versions et que vous n'avez pas encore créé de fichiersecrets.properties
localement pour fournir votre clé API. - Enregistrez le fichier.
- Dans votre fichier
AndroidManifest.xml
, accédez àcom.google.android.geo.API_KEY
et mettez à jour l'attributandroid:value
. Si le tag<meta-data>
n'existe pas, créez-le comme enfant du tag<application>
.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- Dans Android Studio, ouvrez le fichier
build.gradle.kts
au niveau du module et modifiez la propriétésecrets
. Si la propriétésecrets
n'existe pas, ajoutez-la.Modifiez les propriétés du plug-in pour définirpropertiesFileName
sursecrets.properties
,defaultPropertiesFileName
surlocal.defaults.properties
et toute autre propriété.secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. Ajouter Google Maps
Dans cette section, vous allez ajouter une carte Google Maps pour charger la carte lorsque vous lancez l'application.
Ajouter des dépendances Maps Compose
Maintenant que l'application a accès à votre clé API, vous devez ajouter le SDK Maps pour Android en tant que dépendance dans le fichier build.gradle.kts
de votre application. Pour créer votre application avec Jetpack Compose, utilisez la bibliothèque Maps Compose, qui fournit des éléments du SDK Maps pour Android sous forme de fonctions composables et de types de données.
build.gradle.kts
Dans le fichier build.gradle.kts
au niveau de l'application, remplacez les dépendances SDK Maps pour Android non Compose :
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
avec leurs homologues composables :
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
Ajouter un composable Google Maps
Dans MountainMap.kt
, ajoutez le composable GoogleMap
dans le composable Box
imbriqué dans le composable MapMountain
.
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
Créez et exécutez l'application. Et voilà ! Vous devriez voir une carte centrée sur la célèbre Null Island, également connue sous le nom de latitude 0 et longitude 0. Vous apprendrez plus tard à positionner la carte sur l'emplacement et le niveau de zoom souhaités, mais pour l'instant, célébrez votre première victoire !
6. Personnalisation de cartes dans Google Cloud
Vous pouvez personnaliser le style de votre carte en utilisant la Personnalisation de cartes dans Google Cloud.
Créer un ID de carte
Si vous n'avez pas encore créé d'ID de carte associé à un style de carte, consultez le guide relatif aux ID de carte pour effectuer les étapes suivantes :
- Créer un ID de carte
- Associer un ID de carte à un style de carte
Ajouter l'ID de carte à votre application
Pour utiliser l'ID de carte que vous avez créé, lorsque vous instanciez votre composable GoogleMap
, utilisez l'ID de carte lorsque vous créez un objet GoogleMapOptions
qui est attribué au paramètre googleMapOptionsFactory
dans le constructeur.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
Une fois ces étapes terminées, exécutez l'application pour afficher votre carte dans le style que vous avez sélectionné.
7. Charger les données des repères
La tâche principale de l'application consiste à charger une collection de montagnes à partir du stockage local et à les afficher dans GoogleMap
. Dans cette étape, vous allez découvrir l'infrastructure fournie pour charger les données de montagne et les présenter à l'UI.
Montagne
La classe de données Mountain
contient toutes les données sur chaque montagne.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
Notez que les montagnes seront ensuite partitionnées en fonction de leur altitude. Les montagnes d'au moins 4 267 mètres de haut sont appelées fourteeners. Le code de démarrage inclut une fonction d'extension qui effectue cette vérification pour vous.
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
La classe MountainsScreenViewState
contient toutes les données nécessaires pour afficher la vue. Il peut être dans un état Loading
ou MountainList
selon que la liste des montagnes a fini de se charger ou non.
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
Classes fournies : MountainsRepository
et MountainsViewModel
Dans le projet initial, la classe MountainsRepository
a déjà été fournie. Cette classe lit une liste de lieux de montagne stockés dans un fichier GPS Exchange Format
ou GPX, top_peaks.gpx
. L'appel mountainsRepository.loadMountains()
renvoie un StateFlow<List<Mountain>>
.
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
est une classe ViewModel
qui charge les collections de montagnes et les expose, ainsi que d'autres parties de l'état de l'UI via mountainsScreenViewState
. mountainsScreenViewState
est un StateFlow
actif que l'UI peut observer en tant qu'état mutable à l'aide de la fonction d'extension collectAsState
.
En suivant des principes architecturaux solides, MountainsViewModel
contient tout l'état de l'application. L'UI envoie les interactions de l'utilisateur au ViewModel à l'aide de la méthode onEvent
.
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
Si vous souhaitez en savoir plus sur l'implémentation de ces classes, vous pouvez y accéder sur GitHub ou ouvrir les classes MountainsRepository
et MountainsViewModel
dans Android Studio.
Utiliser le ViewModel
Le modèle de vue est utilisé dans MainActivity
pour obtenir le viewState
. Vous utiliserez viewState
pour afficher les repères plus tard dans cet atelier de programmation. Notez que ce code est déjà inclus dans le projet de démarrage et qu'il est présenté ici à titre de référence uniquement.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Positionner la caméra
Un GoogleMap
par défaut est centré sur la latitude 0 et la longitude 0. Les repères que vous allez afficher se trouvent dans l'État du Colorado, aux États-Unis. Le viewState
fourni par le modèle de vue présente un LatLngBounds qui contient tous les repères.
Dans MountainMap.kt
, créez un CameraPositionState
initialisé au centre du cadre de sélection. Définissez le paramètre cameraPositionState
de GoogleMap
sur la variable cameraPositionState
que vous venez de créer.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Exécutez maintenant le code et regardez la carte se centrer sur le Colorado.
Zoomer sur les limites du repère
Pour vraiment centrer la carte sur les repères, ajoutez la fonction zoomAll
à la fin du fichier MountainMap.kt
. Notez que cette fonction a besoin d'un CoroutineScope
, car l'animation de la caméra vers un nouvel emplacement est une opération asynchrone qui prend du temps.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
Ensuite, ajoutez du code pour appeler la fonction zoomAll
chaque fois que les limites autour de la collection de repères changent ou lorsque l'utilisateur clique sur le bouton de zoom étendu dans la barre d'application supérieure. Notez que le bouton "Zoom étendu" est déjà configuré pour envoyer des événements au ViewModel. Il vous suffit de collecter ces événements à partir du ViewModel et d'appeler la fonction zoomAll
en réponse.
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
Lorsque vous exécuterez l'application, la carte sera centrée sur la zone où les repères seront placés. Vous pouvez repositionner la carte et modifier le niveau de zoom. Si vous cliquez sur le bouton "Zoom étendu", la carte sera recentrée sur la zone des repères. Vous avez fait des progrès ! Mais la carte doit vraiment avoir quelque chose à afficher. C'est ce que vous allez faire à l'étape suivante.
9. Repères de base
Dans cette étape, vous allez ajouter des repères à la carte pour représenter les points d'intérêt que vous souhaitez mettre en évidence. Vous allez utiliser la liste des montagnes fournie dans le projet de démarrage et ajouter ces lieux sous forme de repères sur la carte.
Commencez par ajouter un bloc de contenu à GoogleMap
. Il existe plusieurs types de repères. Ajoutez donc une instruction when
pour créer une branche pour chaque type. Vous les implémenterez chacun à votre tour dans les étapes suivantes.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Ajouter des repères
Annotez la variable BasicMarkersMapContent
avec @GoogleMapComposable
. Notez que vous ne pouvez utiliser que des fonctions @GoogleMapComposable
dans le bloc de contenu GoogleMap
. L'objet mountains
contient une liste d'objets Mountain
. Vous allez ajouter un repère pour chaque montagne de cette liste, en utilisant l'emplacement, le nom et l'altitude de l'objet Mountain
. L'emplacement est utilisé pour définir le paramètre d'état de Marker
, qui contrôle à son tour la position du repère.
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
Exécutez l'application. Vous verrez les repères que vous venez d'ajouter.
Personnaliser les repères
Il existe plusieurs options de personnalisation vous permettant de mettre en valeur les repères que vous venez d'ajouter et de fournir des informations pertinentes aux utilisateurs. Dans cette tâche, vous allez découvrir quelques-unes de ces options en personnalisant l'image de chaque repère.
Le projet de démarrage inclut une fonction d'assistance, vectorToBitmap
, pour créer des BitmapDescriptor
à partir d'un @DrawableResource
.
Le code de démarrage inclut une icône de montagne, baseline_filter_hdr_24.xml
, que vous utiliserez pour personnaliser les repères.
La fonction vectorToBitmap
convertit un élément vectoriel en BitmapDescriptor
pour l'utiliser avec la bibliothèque Maps. Les couleurs des icônes sont définies à l'aide d'une instance BitmapParameters
.
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
Utilisez la fonction vectorToBitmap
pour créer deux BitmapDescriptor
personnalisés : un pour les sommets de plus de 4 000 mètres et un pour les montagnes classiques. Utilisez ensuite le paramètre icon
du composable Marker
pour définir l'icône. Définissez également le paramètre anchor
pour modifier l'emplacement du point d'ancrage par rapport à l'icône. Il est préférable d'utiliser le centre pour ces icônes circulaires.
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
Exécutez l'application et admirez les repères personnalisés. Activez le bouton Show all
pour afficher l'ensemble des montagnes. Les montagnes sont indiquées par des marqueurs différents selon qu'il s'agit d'un "fourteener" ou non.
10. Repères avancés
Les AdvancedMarker
ajoutent des fonctionnalités supplémentaires à Markers
de base. Au cours de cette étape, vous allez définir le comportement en cas de chevauchement et configurer le style du repère.
Ajoutez @GoogleMapComposable
à la fonction AdvancedMarkersMapContent
. Effectuez une boucle sur mountains
en ajoutant un AdvancedMarker
pour chacun.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
Notez le paramètre collisionBehavior
. Si vous définissez ce paramètre sur REQUIRED_AND_HIDES_OPTIONAL
, votre repère remplacera tout repère de priorité inférieure. Pour vous en rendre compte, effectuez un zoom avant sur un repère de base et un repère avancé. Le repère de base sera probablement placé au même endroit que votre repère dans la carte de base. Le repère avancé masquera le repère de priorité inférieure.
Exécutez l'application pour voir les repères avancés. Veillez à sélectionner l'onglet Advanced markers
dans la rangée de navigation inférieure.
AdvancedMarkers
Personnalisé
Les icônes utilisent les schémas de couleurs primaires et secondaires pour distinguer les sommets de plus de 14 000 pieds des autres montagnes. Utilisez la fonction vectorToBitmap
pour créer deux BitmapDescriptor
: un pour les quatorze mille et un pour les autres montagnes. Utilisez ces icônes pour créer un pinConfig
personnalisé pour chaque type. Enfin, appliquez l'épingle à la AdvancedMarker
correspondante en fonction de la fonction is14er()
.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. Marqueurs regroupés
Dans cette étape, vous allez utiliser le composable Clustering
pour ajouter le regroupement d'éléments basé sur le zoom.
Le composable Clustering
nécessite une collection de ClusterItem
. MountainClusterItem
implémente l'interface ClusterItem
. Ajoutez cette classe au fichier ClusteringMarkersMapContent.kt
.
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
Ajoutez maintenant le code pour créer des MountainClusterItem
à partir de la liste des montagnes. Notez que ce code utilise un UnitsConverter
pour effectuer la conversion en unités d'affichage adaptées à l'utilisateur en fonction de ses paramètres régionaux. Cette configuration est effectuée dans MainActivity
à l'aide d'un CompositionLocal
.
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
Avec ce code, les repères sont regroupés en fonction du niveau de zoom. C'est propre !
Personnaliser les clusters
Comme pour les autres types de repères, les repères regroupés sont personnalisables. Le paramètre clusterItemContent
du composable Clustering
définit un bloc composable personnalisé pour afficher un élément non regroupé. Implémentez une fonction @Composable
pour créer le repère. La fonction SingleMountain
affiche un composable Icon
Material 3 avec un jeu de couleurs d'arrière-plan personnalisé.
Dans ClusteringMarkersMapContent.kt
, créez une classe de données définissant le jeu de couleurs d'un repère :
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
De plus, dans ClusteringMarkersMapContent.kt
, créez une fonction composable pour afficher une icône pour un jeu de couleurs donné :
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
Créez maintenant un jeu de couleurs pour les sommets de plus de 4 000 mètres et un autre pour les autres montagnes. Dans le bloc clusterItemContent
, sélectionnez la palette de couleurs en fonction de l'altitude de la montagne.
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
Exécutez maintenant l'application pour afficher les versions personnalisées des éléments individuels.
12. Dessiner sur la carte
Vous avez déjà exploré une méthode pour dessiner sur la carte (en ajoutant des repères), mais le SDK Maps pour Android propose de nombreuses autres façons d'afficher des informations utiles.
Par exemple, si vous souhaitez afficher des itinéraires et des zones sur la carte, vous pouvez utiliser des Polyline
et des Polygon
. Si vous souhaitez appliquer une image à la surface du sol, vous pouvez également utiliser un GroundOverlay
.
Dans cette tâche, vous allez apprendre à dessiner des formes, plus précisément un contour autour de l'État du Colorado. La frontière du Colorado est définie entre 37° N et 41° N de latitude, et entre 102°03' W et 109°03' W de longitude. Cela simplifie considérablement le tracé du contour.
Le code de démarrage inclut une classe DMS
pour convertir la notation en degrés-minutes-secondes en degrés décimaux.
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
Avec la classe DMS, vous pouvez dessiner la frontière du Colorado en définissant les quatre emplacements LatLng
des angles et en les affichant sous forme de Polygon
. Ajoutez le code suivant à MountainMap.kt
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
Appelez maintenant ColoradoPolyon()
dans le bloc de contenu GoogleMap
.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
L'application affiche désormais le contour de l'État du Colorado avec un remplissage subtil.
13. Ajouter un calque KML et une barre d'échelle
Dans cette dernière section, vous allez définir approximativement les différentes chaînes de montagnes et ajouter une barre d'échelle à la carte.
Entourez les chaînes de montagnes.
Auparavant, vous avez dessiné un contour autour du Colorado. Vous allez maintenant ajouter des formes plus complexes à la carte. Le code de démarrage inclut un fichier KML (Keyhole Markup Language) qui décrit approximativement les chaînes de montagnes importantes. La bibliothèque d'utilitaires du SDK Maps pour Android dispose d'une fonction permettant d'ajouter un calque KML à la carte. Dans MountainMap.kt
, ajoutez un appel MapEffect
dans le bloc de contenu GoogleMap
après le bloc when
. La fonction MapEffect
est appelée avec un objet GoogleMap
. Il peut servir de pont utile entre les API et les bibliothèques non composables qui nécessitent un objet GoogleMap
.
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
Ajouter une échelle de carte
Pour terminer, vous allez ajouter une échelle à la carte. ScaleBar
implémente un composable d'échelle qui peut être ajouté à la carte. Notez que ScaleBar
n'est pas
@GoogleMapComposable
et ne peut donc pas être ajouté au contenu GoogleMap
. Vous devez l'ajouter à l'Box
qui contient la carte.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Exécutez l'application pour voir l'atelier de programmation entièrement implémenté.
14. Télécharger le code de solution
Pour télécharger le code de cet atelier de programmation, utilisez les commandes suivantes :
- Clonez le dépôt si vous avez installé
git
.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Vous pouvez également cliquer sur le bouton suivant pour télécharger le code source.
- Une fois le code obtenu, dans Android Studio, ouvrez le projet qui se trouve dans le répertoire
solution
.
15. Félicitations
Félicitations ! Vous avez terminé un programme bien rempli. Nous espérons que vous connaissez mieux les principales fonctionnalités du SDK Maps pour Android.
En savoir plus
- SDK Maps pour Android : créez des cartes, des lieux et des expériences géospatiales dynamiques, interactifs et personnalisés pour vos applications Android.
- Bibliothèque Maps Compose : ensemble de fonctions composables et de types de données Open Source que vous pouvez utiliser avec Jetpack Compose pour créer votre application.
- android-maps-compose : exemples de code sur GitHub illustrant toutes les fonctionnalités abordées dans cet atelier de programmation, et bien d'autres.
- Autres ateliers de programmation Kotlin pour créer des applications Android avec Google Maps Platform