Ajouter une carte à votre application Android (Kotlin avec Compose)

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

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

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.

  1. Dans Cloud Console, cliquez sur le menu déroulant des projets, puis sélectionnez celui que vous souhaitez utiliser pour cet atelier de programmation.

  1. 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.
  2. 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.

  1. 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.

  1. 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 :

  1. Dans Android Studio, ouvrez votre fichier build.gradle.kts de premier niveau et ajoutez le code suivant à l'élément dependencies sous buildscript.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Ouvrez le fichier build.gradle.kts au niveau du module et ajoutez le code suivant à l'élément plugins.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. Dans le fichier build.gradle.kts au niveau du module, assurez-vous que targetSdk et compileSdk sont définis sur au moins 34.
  4. Enregistrez le fichier et synchronisez votre projet avec Gradle.
  5. Ouvrez le fichier secrets.properties dans votre répertoire de premier niveau, puis ajoutez le code suivant. Remplacez YOUR_API_KEY par votre clé API. Stockez votre clé dans ce fichier, car secrets.properties n'est pas vérifié dans un système de contrôle des versions.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. Enregistrez le fichier.
  7. Créez le fichier local.defaults.properties dans votre répertoire de premier niveau (même dossier que le fichier secrets.properties), puis ajoutez le code suivant.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    Ce fichier a pour but de fournir un emplacement de sauvegarde de la clé API, à utiliser si le fichier 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 fichier secrets.properties localement pour fournir votre clé API.
  8. Enregistrez le fichier.
  9. Dans votre fichier AndroidManifest.xml, accédez à com.google.android.geo.API_KEY et mettez à jour l'attribut android: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}" />
    
  10. 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éfinir propertiesFileName sur secrets.properties, defaultPropertiesFileName sur local.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 :

  1. Créer un ID de carte
  2. 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.

Bouton &quot;Étendues&quot;

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 :

  1. 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.

  1. 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