Ajouter une carte à votre application Android (Kotlin)

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 magasins de vélos à San Francisco, Californie, États-Unis.

f05e1ca27ff42bf6.png

Prerequisites

  • Connaissances de base en développement avec Kotlin et Android

Objectifs de l'atelier

  • Activer et utiliser le SDK Maps pour Android afin d'ajouter Google Maps à une application Android
  • Ajouter, personnaliser et regrouper des marqueurs
  • Tracer des polylignes et des polygones sur la carte
  • Contrôler le focus de la caméra via le programmatique

Prérequis

2. Configuration

Pour cette étape , 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/googlecodelabs/maps-platform-101-android.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. Ajouter Google Maps

Dans cette section, vous allez ajouter Google Maps pour charger la carte lorsque vous lancez l'application.

d1d068b5d4ae38b9.png

Ajouter votre clé API

La clé API que vous avez créée précédemment doit être fournie à l'application afin que le SDK Maps pour Android puisse associer ces deux éléments.

  1. Pour ce faire, ouvrez le fichier nommé local.properties dans le répertoire racine de votre projet (soit au même niveau que gradle.properties et settings.gradle).
  2. Dans ce fichier, définissez une nouvelle clé GOOGLE_MAPS_API_KEY avec pour valeur la clé API que vous avez créée.

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Notez que local.properties est répertorié dans le fichier .gitignore du dépôt Git. En effet, votre clé API est considérée comme une information sensible et ne doit pas être vérifiée dans un contrôle du code source, si possible.

  1. Pour exposer ensuite votre API afin de l'utiliser dans l'ensemble de votre application, incluez le plug-in Secrets Gradle pour Android dans le fichier build.gradle de votre application (situé dans le répertoire app/) et ajoutez la ligne suivante dans le bloc plugins :

build.gradle au niveau de l'application

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

Vous devrez également modifier le fichier build.gradle au niveau du projet pour inclure le chemin de classe suivant:

build.gradle au niveau du projet

buildscript {
    dependencies {
        // ...
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
    }
}

Avec ce plug-in, les clés que vous avez définies dans votre fichier local.properties seront disponibles en tant que variables de compilation dans le fichier manifeste Android et en tant que variables dans la classe BuildConfig générée par Gradle au moment de la compilation. Ainsi, vous n'avez plus besoin du code récurrent permettant de lire les propriétés de local.properties afin d'étendre l'accès à l'ensemble de l'application.

Ajouter la dépendance Google Maps

  1. 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 de votre application.

Dans le projet initial proposé dans cet atelier de programmation, cette dépendance a déjà été ajoutée.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Ajoutez ensuite une nouvelle balise meta-data dans AndroidManifest.xml pour transmettre la clé API que vous avez créée précédemment. Pour ce faire, ouvrez ce fichier dans Android Studio et ajoutez la balise meta-data suivante dans l'objet application de votre fichier AndroidManifest.xml, situé dans app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. Créez ensuite un fichier de mise en page appelé activity_main.xml dans le répertoire app/src/main/res/layout/ et définissez-le comme suit :

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <fragment
       class="com.google.android.gms.maps.SupportMapFragment"
       android:id="@+id/map_fragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</FrameLayout>

Cette mise en page comporte un seul FrameLayout contenant un SupportMapFragment, qui inclut l'objet GoogleMaps sous-jacent que vous utiliserez dans les étapes suivantes.

  1. Enfin, mettez à jour la classe MainActivity située dans app/src/main/java/com/google/codelabs/buildyourfirstmap en ajoutant le code suivant pour ignorer la méthode onCreate, ce qui vous permettra de définir son contenu avec la mise en page que vous venez de créer.

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Exécutez à présent l'application. La carte devrait se charger sur l'écran de votre appareil.

5. Personnalisation de cartes dans Google Cloud (facultatif)

Vous pouvez personnaliser le style de votre carte à l'aide de la fonctionnalité 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 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 dans votre application

Pour utiliser l'identifiant de carte que vous avez créé, modifiez le fichier activity_main.xml et transmettez votre identifiant de carte dans l'attribut map:mapId de SupportMapFragment.

activity_main.xml

<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
    class="com.google.android.gms.maps.SupportMapFragment"
    <!-- ... -->
    map:mapId="YOUR_MAP_ID" />

Une fois ces étapes terminées, exécutez l'application pour afficher votre carte dans le style que vous avez sélectionné.

6. Ajouter des repères

Dans cette tâche, vous allez ajouter les repères qui représentent les points d'intérêt que vous souhaitez mettre en évidence sur la carte. Pour commencer, vous devez extraire la liste des lieux fournis dans le projet initial, puis les ajouter à la carte. Dans cet exemple, il s'agit de magasins de vélos.

bc5576877369b554.png

Obtenir une référence à GoogleMap

Tout d'abord, vous devez obtenir une référence à l'objet GoogleMap afin de pouvoir utiliser ses méthodes. Pour cela, ajoutez le code suivant dans votre méthode MainActivity.onCreate(), juste après l'appel de setContentView() :

MainActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

L'implémentation commence par rechercher le SupportMapFragment que vous avez ajouté à l'étape précédente en utilisant la méthode findFragmentById() sur l'objet SupportFragmentManager. Une fois qu'elle a obtenu une référence, elle appelle getMapAsync(), puis transmet un lambda, où l'objet GoogleMap est transmis. Dans ce lambda, la méthode addMarkers() est appelée. Nous la définirons bientôt.

Classe fournie : PlacesReader

Dans le projet initial, la classe PlacesReader a déjà été fournie. Elle lit une liste de 49 lieux stockés dans un fichier JSON appelé places.json et les renvoie en tant que List<Place>. Il s'agit de magasins de vélos à San Francisco, en Californie, États-Unis.

Si vous souhaitez en savoir plus sur l'implémentation de la classe PlacesReader, vous pouvez y accéder sur GitHub ou l'ouvrir dans Android Studio.

PlacesReader

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader

/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {

   // GSON object responsible for converting from JSON to a Place object
   private val gson = Gson()

   // InputStream representing places.json
   private val inputStream: InputStream
       get() = context.resources.openRawResource(R.raw.places)

   /**
    * Reads the list of place JSON objects in the file places.json
    * and returns a list of Place objects
    */
   fun read(): List<Place> {
       val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
       val reader = InputStreamReader(inputStream)
       return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
           it.toPlace()
       }
   }

Charger les lieux

Pour charger la liste des magasins de vélos, ajoutez une propriété appelée places dans MainActivity, puis définissez-la comme suit :

MainActivity.places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

Ce code appelle la méthode read() sur PlacesReader, qui renvoie un List<Place>. Chaque Place comprend une propriété name (le nom du lieu) et une propriété latLng (les coordonnées du lieu).

Place

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: LatLng,
   val rating: Float
)

Ajouter des repères à la carte

Maintenant que les lieux ont été chargés dans la mémoire, l'étape suivante consiste à les représenter sur la carte.

  1. Dans MainActivity, créez une méthode appelée addMarkers() et définissez-la comme suit :

MainActivity.addMarkers()

/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
   places.forEach { place ->
       val marker = googleMap.addMarker(
           MarkerOptions()
               .title(place.name)
               .position(place.latLng)
       )
   }
}

Cette méthode itère la liste de places, puis appelle la méthode addMarker() sur l'objet GoogleMap fourni. Le repère est créé en instanciant un objet MarkerOptions, qui vous permet de le personnaliser. Dans ce cas, le titre et la position du repère sont indiqués. Ils représentent respectivement le nom les coordonnées du magasin de vélos.

  1. Exécutez l'application et affichez San Francisco pour voir les repères que vous venez d'ajouter.

7. 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 des repères, ainsi que la fenêtre d'informations qui s'affiche lorsque l'utilisateur appuie sur l'un d'eux.

a26f82802fe838e9.png

Ajouter une fenêtre d'informations

Lorsque vous appuyez sur un repère, la fenêtre d'informations affiche par défaut le titre et l'extrait associés (s'ils sont définis). Avec cette option, vous pouvez la personnaliser pour fournir des renseignements supplémentaires tels que l'adresse et la note.

Créer marker_info_contents.xml

Commencez par créer un fichier de mise en page appelé marker_info_contents.xml.

  1. Pour ce faire, effectuez un clic droit sur le dossier app/src/main/res/layout dans la vue du projet dans Android Studio, puis sélectionnez New (Nouveau) > Layout Resource File (Fichier de ressource de mise en page).

8cac51fcbef9171b.png

  1. Dans la boîte de dialogue, saisissez marker_info_contents dans le champ File name (Nom du fichier) et LinearLayout dans le champ Root element (Élément racine), puis cliquez sur OK.

8783af12baf07a80.png

Ce fichier de mise en page est ensuite gonflé pour représenter le contenu dans la fenêtre d'informations.

  1. Copiez le contenu dans l'extrait de code suivant, qui ajoute trois TextViews dans un groupe d'affichage vertical LinearLayout et écrase le code par défaut dans le fichier.

marker_info_contents.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:orientation="vertical"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:gravity="center_horizontal"
   android:padding="8dp">

   <TextView
       android:id="@+id/text_view_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="18sp"
       android:textStyle="bold"
       tools:text="Title"/>

   <TextView
       android:id="@+id/text_view_address"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="123 Main Street"/>

   <TextView
       android:id="@+id/text_view_rating"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="Rating: 3"/>

</LinearLayout>

Créer une implémentation d'InfoWindowAdapter

Après avoir créé le fichier de mise en page pour la fenêtre d'informations personnalisée, l'étape suivante consiste à implémenter l'interface GoogleMap.InfoWindowAdapter. Cette interface contient deux méthodes : getInfoWindow() et getInfoContents(). Elles renvoient toutes deux un objet View facultatif, dans lequel la première méthode est utilisée pour personnaliser la fenêtre elle-même et la seconde pour personnaliser son contenu. Dans cet atelier, vous allez implémenter les deux méthodes et personnaliser le résultat de getInfoContents() en renvoyant la valeur nulle dans getInfoWindow(), ce qui indique que la fenêtre par défaut doit être utilisée.

  1. Créez un fichier Kotlin nommé MarkerInfoWindowAdapter dans le même package que MainActivity. Pour cela, effectuez un clic droit sur le dossier app/src/main/java/com/google/codelabs/buildyourfirstmap dans la vue du projet dans Android Studio, puis sélectionnez New (Nouveau) > Kotlin File/Class (Ficher/Classe Kotlin).

3975ba36eba9f8e1.png

  1. Dans la boîte de dialogue, saisissez MarkerInfoWindowAdapter et laissez l'option File (Fichier) en surbrillance.

992235af53d3897f.png

  1. Une fois le fichier créé, copiez dans celui-ci le contenu de l'extrait de code suivant.

MarkerInfoWindowAdapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place

class MarkerInfoWindowAdapter(
    private val context: Context
) : GoogleMap.InfoWindowAdapter {
   override fun getInfoContents(marker: Marker?): View? {
       // 1. Get tag
       val place = marker?.tag as? Place ?: return null

       // 2. Inflate view and set title, address, and rating
       val view = LayoutInflater.from(context).inflate(
           R.layout.marker_info_contents, null
       )
       view.findViewById<TextView>(
           R.id.text_view_title
       ).text = place.name
       view.findViewById<TextView>(
           R.id.text_view_address
       ).text = place.address
       view.findViewById<TextView>(
           R.id.text_view_rating
       ).text = "Rating: %.2f".format(place.rating)

       return view
   }

   override fun getInfoWindow(marker: Marker?): View? {
       // Return null to indicate that the
       // default window (white bubble) should be used
       return null
   }
}

Dans le contenu de la méthode getInfoContents(), le repère fourni dans la méthode est casté en type Place. S'il n'est pas possible de le caster, la méthode renvoie une valeur nulle. Vous n'avez pas encore défini la propriété de la balise sur le repère (Marker), mais vous le ferez à l'étape suivante.

Ensuite, la mise en page marker_info_contents.xml est gonflée, puis le texte sur les TextViews correspondants est défini sur la balise Place.

Mettre à jour MainActivity

Pour assembler tous les composants que vous avez créés jusqu'à présent, vous devez ajouter deux lignes dans votre classe MainActivity.

Tout d'abord, pour transmettre l'InfoWindowAdapter personnalisé, MarkerInfoWindowAdapter, dans l'appel de la méthode getMapAsync, appelez la méthode setInfoWindowAdapter() sur l'objet GoogleMap et créez une instance de MarkerInfoWindowAdapter.

  1. Pour ce faire, ajoutez le code suivant après l'appel de la méthode addMarkers() dans le lambda getMapAsync().

MainActivity.onCreate()

// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

Enfin, vous devez définir chaque "Place" en tant que propriété de balise à chaque "Marker" ajouté à la carte.

  1. Pour ce faire, modifiez l'appel à places.forEach{} dans la fonction addMarkers() comme suit :

MainActivity.addMarkers()

places.forEach { place ->
   val marker = googleMap.addMarker(
       MarkerOptions()
           .title(place.name)
           .position(place.latLng)
           .icon(bicycleIcon)
   )

   // Set place as the tag on the marker object so it can be referenced within
   // MarkerInfoWindowAdapter
   marker.tag = place
}

Ajouter une image de repère personnalisée

Personnaliser l'image du repère permet de communiquer de manière conviviale le type de lieu qu'il représente sur votre carte. Dans cette étape, vous allez afficher des vélos à la place des repères rouges par défaut pour représenter chaque magasin sur la carte. Le projet initial inclut dans app/src/res/drawable l'icône Vélo ic_directions_bike_black_24dp.xml que vous utiliserez.

6eb7358bb61b0a88.png

Définir le bitmap personnalisé sur le repère

L'icône Vélo étant à votre disposition sous forme de drawable vectoriel, l'étape suivante consiste à le définir pour chaque repère sur la carte. Pour cela, MarkerOptions comporte une méthode icon, qui prend un BitmapDescriptor.

Tout d'abord, vous devez convertir le drawable vectoriel que vous venez d'ajouter dans un BitmapDescriptor. Un fichier nommé BitMapHelper inclus dans le projet initial contient une fonction d'assistance appelée vectorToBitmap() conçue pour réaliser cette tâche.

BitmapHelper

package com.google.codelabs.buildyourfirstmap

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory

object BitmapHelper {
   /**
    * Demonstrates converting a [Drawable] to a [BitmapDescriptor],
    * for use as a marker icon. Taken from ApiDemos on GitHub:
    * https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
    */
   fun vectorToBitmap(
      context: Context,
      @DrawableRes id: Int,
      @ColorInt color: Int
   ): BitmapDescriptor {
       val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
       if (vectorDrawable == null) {
           Log.e("BitmapHelper", "Resource not found")
           return BitmapDescriptorFactory.defaultMarker()
       }
       val bitmap = Bitmap.createBitmap(
           vectorDrawable.intrinsicWidth,
           vectorDrawable.intrinsicHeight,
           Bitmap.Config.ARGB_8888
       )
       val canvas = Canvas(bitmap)
       vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
       DrawableCompat.setTint(vectorDrawable, color)
       vectorDrawable.draw(canvas)
       return BitmapDescriptorFactory.fromBitmap(bitmap)
   }
}

Cette méthode utilise un Context, un ID de ressource drawable et une couleur sous forme d'un entier, et crée une représentation BitmapDescriptor du drawable vectoriel.

Déclarer une nouvelle propriété bicycleIcon et lui attribuer la définition MainActivity.bicycleicon avec la méthode d'assistance

private val bicycleIcon: BitmapDescriptor by lazy {
   val color = ContextCompat.getColor(this, R.color.colorPrimary)
   BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}

Cette propriété utilise la couleur prédéfinie colorPrimary de votre application pour teinter l'icône Vélo et la renvoyer en tant que BitmapDescriptor.

  1. Avec cette propriété, vous pouvez maintenant appeler la méthode icon de MarkerOptions dans la méthode addMarkers() pour terminer de personnaliser l'icône. La propriété du repère devrait alors ressembler à ceci :

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Lancez l'application pour voir les nouveaux repères.

8. Regrouper les repères

Selon le niveau de zoom sur la carte, vous remarquez peut-être que les repères que vous avez ajoutés se chevauchent. Les interactions deviennent alors difficiles, et les repèrent gênent la vue ce qui nuit à l'utilisabilité de votre application.

68591edc86d73724.png

Pour améliorer l'expérience utilisateur, dès lors que vous disposez d'un ensemble de données volumineux et compact, regrouper les repères est une bonne pratique. Avec cette méthode, lorsque vous faites un zoom avant ou arrière sur la carte, les repères proches les uns des autres sont regroupés comme suit :

f05e1ca27ff42bf6.png

Pour implémenter le regroupement des repères, vous avez besoin de la bibliothèque d'utilitaires du SDK Maps pour Android.

Bibliothèque d'utilitaires du SDK Maps pour Android

La bibliothèque d'utilitaires a été créée pour élargir les possibilités offertes par le SDK Maps pour Android. Elle propose des fonctionnalités avancées telles que le regroupement de repères, les cartes de densité, la prise en charge de KML et de GeoJson, l'encodage et le décodage de polylignes, et quelques fonctions d'assistance pour la géométrie sphérique.

Mettre à jour build.gradle

Étant donné que la bibliothèque d'utilitaires n'est pas fournie dans le même package que le SDK Maps pour Android, vous devez ajouter une dépendance à votre fichier build.gradle.

  1. Pour cela, modifiez la section dependencies de votre fichier app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Une fois que vous avez ajouté cette ligne, vous devez synchroniser le projet pour récupérer les nouvelles dépendances.

b7b030ec82c007fd.png

Implémenter le regroupement

Pour regrouper les repères dans votre application, suivez ces trois étapes :

  1. Implémentez l'interface ClusterItem.
  2. Créez une sous-classe de la classe DefaultClusterRenderer.
  3. Créez un ClusterManager et ajoutez des éléments.

Implémenter l'interface ClusterItem

Tous les objets représentant un repère regroupable sur la carte doivent implémenter l'interface ClusterItem. Dans votre cas, cela signifie que le modèle Place doit être conforme à ClusterItem. Ouvrez le fichier Place.kt et modifiez-le comme suit :

Place

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: String,
   val rating: Float
) : ClusterItem {
   override fun getPosition(): LatLng =
       latLng

   override fun getTitle(): String =
       name

   override fun getSnippet(): String =
       address
}

L'objet ClusterItem définit les trois méthodes suivantes :

  • getPosition(), qui représente les coordonnées LatLng du lieu
  • getTitle(), qui représente le nom du lieu
  • getSnippet(), qui représente l'adresse du lieu

Créer une sous-classe de la classe DefaultClusterRenderer

La classe chargée d'implémenter le regroupement, ClusterManager, utilise en interne une classe ClusterRenderer pour regrouper les repères lorsque vous déplacez et zoomez la carte. Par défaut, elle utilise le moteur de rendu DefaultClusterRenderer qui implémente ClusterRenderer. Dans les scénarios simples, cette classe est suffisante. Toutefois, dans votre cas, étant donné que les repères doivent être personnalisés, vous devez étendre cette classe et y ajouter les personnalisations nécessaires.

Créez le fichier Kotlin PlaceRenderer.kt dans le package com.google.codelabs.buildyourfirstmap.place, puis définissez-le comme suit :

PlaceRenderer

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer

/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
   private val context: Context,
   map: GoogleMap,
   clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {

   /**
    * The icon to use for each cluster item
    */
   private val bicycleIcon: BitmapDescriptor by lazy {
       val color = ContextCompat.getColor(context,
           R.color.colorPrimary
       )
       BitmapHelper.vectorToBitmap(
           context,
           R.drawable.ic_directions_bike_black_24dp,
           color
       )
   }

   /**
    * Method called before the cluster item (the marker) is rendered.
    * This is where marker options should be set.
    */
   override fun onBeforeClusterItemRendered(
      item: Place,
      markerOptions: MarkerOptions
   ) {
       markerOptions.title(item.name)
           .position(item.latLng)
           .icon(bicycleIcon)
   }

   /**
    * Method called right after the cluster item (the marker) is rendered.
    * This is where properties for the Marker object should be set.
    */
   override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
       marker.tag = clusterItem
   }
}

Cette classe ignore ces deux fonctions :

  • onBeforeClusterItemRendered(), qui est appelée avant le rendu du regroupement sur la carte. C'est là que vous pouvez apporter des personnalisations via MarkerOptions. Dans ce cas, elle définit le titre, la position et l'icône du repère.
  • onClusterItemRenderer(), qui est appelée juste après le rendu du repère sur la carte. C'est là que vous pouvez accéder à l'objet Marker créé. Dans ce cas, elle définit la propriété de balise du repère.

Créer un ClusterManager et ajouter des éléments

Enfin, afin que le regroupement fonctionne, vous devez modifier MainActivity pour instancier un ClusterManager et lui fournir les dépendances nécessaires. ClusterManager ajoute les repères (les objets ClusterItem) en interne. Ainsi, au lieu d'être ajoutés directement sur la carte, les repères sont positionnés par ClusterManager. En outre, ClusterManager appelle également setInfoWindowAdapter() en interne. Par conséquent, la définition d'une fenêtre d'informations personnalisée doit être effectuée sur l'objet MarkerManager.Collection ClusterManger.

  1. Pour commencer, modifiez le contenu du lambda de l'appel de getMapAsync() dans MainActivity.onCreate(). Vous pouvez maintenant mettre l'appel de addMarkers() et de setInfoWindowAdapter() en commentaire, puis appeler une méthode nommée addClusteredMarkers(), que vous définirez ensuite.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
    //addMarkers(googleMap)
    addClusteredMarkers(googleMap)

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Ensuite, dans MainActivity, définissez addClusteredMarkers().

MainActivity.addClusteredMarkers()

/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
   // Create the ClusterManager class and set the custom renderer.
   val clusterManager = ClusterManager<Place>(this, googleMap)
   clusterManager.renderer =
       PlaceRenderer(
           this,
           googleMap,
           clusterManager
       )

   // Set custom info window adapter
   clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

   // Add the places to the ClusterManager.
   clusterManager.addItems(places)
   clusterManager.cluster()

   // Set ClusterManager as the OnCameraIdleListener so that it
   // can re-cluster when zooming in and out.
   googleMap.setOnCameraIdleListener {
       clusterManager.onCameraIdle()
   }
}

Cette méthode instancie un ClusterManager, lui transmet le moteur de rendu personnalisé PlacesRenderer, ajoute tous les lieux, puis appelle la méthode cluster(). De plus, comme ClusterManager utilise la méthode setInfoWindowAdapter() sur l'objet de carte, la définition de la fenêtre d'informations personnalisée doit être effectuée sur l'objet ClusterManager.markerCollection. Enfin, comme vous souhaitez modifier le regroupement lorsque l'utilisateur déplace et zoome la carte, OnCameraIdleListener est fourni à googleMap pour appeler clusterManager.onCameraIdle() quand l'appareil photo est inactif.

  1. Lancez l'application pour voir les magasins regroupés.

9. 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 polylignes et des polygones. Si vous souhaitez appliquer une image à la surface du sol, vous pouvez également utiliser des superpositions au sol.

Dans cette tâche, vous allez apprendre à dessiner des formes. Plus précisément, vous allez tracer un cercle à afficher autour d'un repère lorsque l'utilisateur appuie dessus.

f98ce13055430352.png

Ajouter un écouteur de clics

Généralement, pour ajouter un écouteur de clics à un repère, il faut le transmettre directement à l'objet GoogleMap via setOnMarkerClickListener(). Toutefois, comme vous utilisez le regroupement, l'écouteur de clics doit être fourni à ClusterManager.

  1. Dans la méthode addClusteredMarkers() de MainActivity, ajoutez la ligne suivante juste après l'appel de cluster().

MainActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

Cette méthode ajoute un écouteur et appelle la méthode addCircle(), que vous définirez ensuite. Enfin, elle renvoie false pour indiquer qu'elle n'a pas utilisé cet événement.

  1. Ensuite, vous devez définir la propriété circle et la méthode addCircle() dans MainActivity.

MainActivity.addCircle()

private var circle: Circle? = null

/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
   circle?.remove()
   circle = googleMap.addCircle(
       CircleOptions()
           .center(item.latLng)
           .radius(1000.0)
           .fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
           .strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
   )
}

La propriété circle est définie pour supprimer le cercle précédent et en ajouter un chaque fois qu'un nouveau repère est sélectionné. Notez que l'API qui permet d'ajouter un cercle est très semblable à celle qui permet d'ajouter un repère.

  1. Exécutez l'application pour voir les modifications.

10. Contrôler la caméra

Pour votre dernière tâche, vous découvrirez certaines commandes de la caméra qui vous permettront de centrer la vue sur une région spécifique.

Caméra et vue

Vous l'avez peut-être remarqué, lorsque vous exécutez l'application, la caméra affiche le continent africain. Vous devez prendre le temps de déplacer la carte et de zoomer sur San Francisco pour trouver les repères que vous avez ajoutés. Même si cela peut être un bon moyen d'explorer le monde en s'amusant, vous préférerez sans doute afficher directement les repères.

Pour cela, vous pouvez régler la position de la caméra via le programmatique afin de centrer la vue à l'endroit qui vous intéresse.

  1. Ajoutez le code suivant à l'appel de getMapAsync() pour ajuster la vue de la caméra afin qu'elle soit initialisée sur San Francisco au lancement de l'application.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
   // Ensure all places are visible in the map.
   googleMap.setOnMapLoadedCallback {
       val bounds = LatLngBounds.builder()
       places.forEach { bounds.include(it.latLng) }
       googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
   }
}

D'abord, setOnMapLoadedCallback() est appelé pour que la caméra ne soit mise à jour qu'une fois la carte chargée. Cette étape est nécessaire, car les propriétés de la carte (telles que les dimensions) doivent être calculées avant que la mise à jour de la caméra soit appelée.

Dans le lambda, un nouvel objet LatLngBounds est construit. Il définit une région rectangulaire sur la carte. Cet objet est compilé de manière incrémentale en incluant toutes les valeurs LatLng de "place", afin que la région englobe tous les magasins. Ensuite, la méthode moveCamera() est appelée sur GoogleMap, et un CameraUpdate lui est fourni via CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

  1. Exécutez l'application et notez que la caméra est désormais initialisée sur San Francisco.

Écouter les changements au niveau de la caméra

En plus de modifier la position de la caméra, vous pouvez également écouter les modifications lorsque l'utilisateur se déplace sur la carte. Cela peut être utile si vous voulez modifier l'UI quand la caméra bouge.

Pour vous exercer, vous pouvez modifier le code afin de rendre les repères translucides lorsque la caméra se déplace.

  1. Dans la méthode addClusteredMarkers(), ajoutez les lignes suivantes en bas de la méthode :

MainActivity.addClusteredMarkers()

// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
   clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}

Cela ajoute un OnCameraMoveStartedListener pour que, lorsque la caméra se déplace, toutes les valeurs alpha des repères et des regroupements de repères soient modifiées en 0.3f, ce qui les rend translucides.

  1. Enfin, pour opacifier à nouveau les repères lorsque la caméra ne bouge plus, modifiez le contenu de setOnCameraIdleListener dans la méthode addClusteredMarkers() comme suit :

MainActivity.addClusteredMarkers()

googleMap.setOnCameraIdleListener {
   // When the camera stops moving, change the alpha value back to opaque.
   clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }

   // Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
   // can be performed when the camera stops moving.
   clusterManager.onCameraIdle()
}
  1. Exécutez l'application pour voir le résultat.

11. Maps KTX

Pour les applications Kotlin utilisant un ou plusieurs SDK Android Google Maps Platform, l'extension Kotlin ou les bibliothèques KTX sont disponibles. Vous pouvez ainsi profiter des fonctionnalités du langage Kotlin comme les coroutines, les propriétés/fonctions d'extension, etc. Chaque SDK Google Maps possède une bibliothèque KTX correspondante, comme indiqué ci-dessous :

Diagramme KTX de Google Maps Platform

Dans cette tâche, vous allez utiliser les bibliothèques Maps KTX et Maps Utils KTX avec votre application, puis refactoriser les tâches précédentes afin d'utiliser les fonctionnalités linguistiques spécifiques à Kotlin dans votre application.

  1. Inclure les dépendances KTX dans le fichier build.gradle au niveau de l'application

Étant donné que l'application utilise à la fois le SDK Maps pour Android et la bibliothèque d'utilitaires du SDK Maps pour Android, vous devez inclure les bibliothèques KTX correspondantes pour ces bibliothèques. Vous allez également utiliser une fonctionnalité qui se trouve dans la bibliothèque KXX du cycle de vie AndroidX dans cette tâche. Incluez donc cette dépendance également dans votre fichier build.gradle au niveau de l'application.

build.gradle

dependencies {
    // ...

    // Maps SDK for Android KTX Library
    implementation 'com.google.maps.android:maps-ktx:3.0.0'

    // Maps SDK for Android Utility Library KTX Library
    implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'

    // Lifecycle Runtime KTX Library
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
  1. Utiliser les fonctions d'extension GoogleMap.addMarker() et GoogleMap.addCircle()

La bibliothèque Maps KTX fournit une alternative à l'API de style DSL pour les éléments GoogleMap.addMarker(MarkerOptions) et GoogleMap.addCircle(CircleOptions) utilisés aux étapes précédentes. Pour utiliser les API mentionnées ci-dessus, la création d'une classe contenant des options pour un repère ou un cercle est nécessaire, tandis qu'avec les alternatives KTX, vous pouvez définir les options du repère ou du cercle dans le lambda que vous fournissez.

Pour utiliser ces API, mettez à jour les méthodes MainActivity.addMarkers(GoogleMap) et MainActivity.addCircle(GoogleMap):

MainActivity.addMarkers(GoogleMap)

/**
 * Adds markers to the map. These markers won't be clustered.
 */
private fun addMarkers(googleMap: GoogleMap) {
    places.forEach { place ->
        val marker = googleMap.addMarker {
            title(place.name)
            position(place.latLng)
            icon(bicycleIcon)
        }
        // Set place as the tag on the marker object so it can be referenced within
        // MarkerInfoWindowAdapter
        marker.tag = place
    }
}

MainActivity.addCircle(GoogleMap)

/**
 * Adds a [Circle] around the provided [item]
 */
private fun addCircle(googleMap: GoogleMap, item: Place) {
    circle?.remove()
    circle = googleMap.addCircle {
        center(item.latLng)
        radius(1000.0)
        fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
        strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
    }
}

Réécrire les méthodes ci-dessus de cette manière est beaucoup plus concis, ce qui est rendu possible à l'aide du littéral de fonction avec Kotlin de Kotlin.

  1. Utiliser SupportMapFragment.awaitMap() et GoogleMap.awaitMapLoad() pour les fonctions de suspension d'extensions

La bibliothèque Maps KTX fournit également des extensions de fonction de suspension à utiliser dans les coroutines. Plus précisément, il existe des alternatives aux fonctions de suspension pour SupportMapFragment.getMapAsync(OnMapReadyCallback) et GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). L'utilisation de ces API alternatives ne nécessite pas de transmettre des rappels, ce qui vous permet de recevoir la réponse de ces méthodes de manière synchrone et synchrone.

Étant donné que ces méthodes suspendent des fonctions, elles doivent être utilisées dans une coroutine. La bibliothèque Cycle de vie d'exécution du pipeline KTX propose une extension pour fournir des champs d'application des coroutines adaptées au cycle de vie, afin que les coroutines soient exécutées et arrêtées lors de l'événement de cycle de vie approprié.

En combinant ces concepts, mettez à jour la méthode MainActivity.onCreate(Bundle):

MainActivity.onCreate(Bundle)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val mapFragment =
        supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
    lifecycleScope.launchWhenCreated {
        // Get map
        val googleMap = mapFragment.awaitMap()

        // Wait for map to finish loading
        googleMap.awaitMapLoad()

        // Ensure all places are visible in the map
        val bounds = LatLngBounds.builder()
        places.forEach { bounds.include(it.latLng) }
        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))

        addClusteredMarkers(googleMap)
    }
}

La portée de la coroutine lifecycleScope.launchWhenCreated exécute le bloc lorsque l'activité est au moins dans l'état créé. Notez également que les appels pour récupérer l'objet GoogleMap et pour attendre que le chargement de la carte soit terminé, ont été remplacés par SupportMapFragment.awaitMap() et GoogleMap.awaitMapLoad(), respectivement. La refactorisation à l'aide de ces fonctions de suspension vous permet d'écrire le code basé sur le rappel équivalent de manière séquentielle.

  1. Vous allez devoir recompiler l'application avec vos modifications refactoriséess.

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

Learn more

  • SDK Places pour Android : explorez le vaste ensemble de données de lieux pour découvrir des établissements autour de vous.
  • android-maps-ktx : cette bibliothèque Open Source s'intègre au SDK Maps pour Android et à la bibliothèque d'utilitaires du SDK Maps pour Android, tout en étant compatible avec Kotlin.
  • android-place-ktx : bibliothèque Open Source s'intégrant au SDK Places pour Android, compatible avec Kotlin
  • android-samples : 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