Aggiungere una mappa all'app per Android (Kotlin)

Aggiungere una mappa all'app per Android (Kotlin)

Informazioni su questo codelab

subjectUltimo aggiornamento: set 18, 2024
account_circleScritto da: Google Maps Platform Team

1. Prima di iniziare

Questo codelab ti insegna a integrare Maps SDK for Android nella tua app e a utilizzare le sue funzionalità principali creando un'app che mostra una mappa dei negozi di biciclette a San Francisco, in California, negli Stati Uniti.

f05e1ca27ff42bf6.png

Prerequisiti

  • Conoscenza di base di Kotlin e dello sviluppo per Android

In questo lab proverai a:

  • Attiva e utilizza Maps SDK for Android per aggiungere Google Maps a un'app per Android.
  • Aggiungere, personalizzare e raggruppare i segnaposto.
  • Disegna polilinee e poligoni sulla mappa.
  • Controlla il punto di vista della videocamera in modo programmatico.

Che cosa ti serve

2. Configurazione

Per il seguente passaggio di attivazione , devi abilitare Maps SDK for Android.

Configurare Google Maps Platform

Se non hai ancora un account Google Cloud Platform e un progetto con la fatturazione abilitata, consulta la guida Guida introduttiva a Google Maps Platform per creare un account di fatturazione e un progetto.

  1. Nella console Cloud, fai clic sul menu a discesa del progetto e seleziona il progetto che vuoi utilizzare per questo codelab.

  1. Abilita le API e gli SDK di Google Maps Platform richiesti per questo codelab in Google Cloud Marketplace. Per farlo, segui i passaggi descritti in questo video o in questa documentazione.
  2. Genera una chiave API nella pagina Credenziali di Cloud Console. Puoi seguire i passaggi descritti in questo video o in questa documentazione. Tutte le richieste a Google Maps Platform richiedono una chiave API.

3. Avvio rapido

Per iniziare il più rapidamente possibile, ecco un codice iniziale che ti aiuterà a seguire questo codelab. Puoi passare direttamente alla soluzione, ma se vuoi seguire tutti i passaggi per crearla da solo, continua a leggere.

  1. Clona il repository se hai installato git.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

In alternativa, puoi fare clic sul seguente pulsante per scaricare il codice sorgente.

  1. Una volta ottenuto il codice, apri il progetto che si trova nella directory starter in Android Studio.

4. Aggiungere Google Maps

In questa sezione aggiungerai Google Maps in modo che venga caricato all'avvio dell'app.

d1d068b5d4ae38b9.png

Aggiungere la chiave API

La chiave API che hai creato in un passaggio precedente deve essere fornita all'app in modo che Maps SDK for Android possa associarla alla tua app.

  1. Per fornirlo, apri il file denominato local.properties nella directory principale del progetto (lo stesso livello di gradle.properties e settings.gradle).
  2. In questo file, definisci una nuova chiave GOOGLE_MAPS_API_KEY con il valore della chiave API che hai creato.

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Nota che local.properties è elencato nel file .gitignore nel repository Git. Questo perché la chiave API è considerata un'informazione sensibile e, se possibile, non deve essere archiviata nel controllo del codice sorgente.

  1. Successivamente, per esporre l'API in modo che possa essere utilizzata in tutta l'app, includi il plug-in Secrets Gradle Plugin per Android nel file build.gradle dell'app che si trova nella directory app/ e aggiungi la seguente riga all'interno del blocco plugins:

build.gradle a livello di app

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

Dovrai anche modificare il file build.gradle a livello di progetto per includere il seguente classpath:

build.gradle a livello di progetto

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

Questo plug-in renderà disponibili le chiavi che hai definito nel file local.properties come variabili di build nel file manifest Android e come variabili nella classe BuildConfig generata da Gradle al momento della build. L'utilizzo di questo plug-in rimuove il codice boilerplate che altrimenti sarebbe necessario per leggere le proprietà da local.properties in modo che sia possibile accedervi in tutta l'app.

Aggiungere la dipendenza di Google Maps

  1. Ora che è possibile accedere alla chiave API all'interno dell'app, il passaggio successivo consiste nell'aggiungere la dipendenza dell'SDK Maps per Android al file build.gradle dell'app.

In questo progetto iniziale fornito con questo codelab, questa dipendenza è già stata aggiunta.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Successivamente, aggiungi un nuovo tag meta-data in AndroidManifest.xml per inserire la chiave API che hai creato in un passaggio precedente. Per farlo, apri questo file in Android Studio e aggiungi il seguente tag meta-data all'interno dell'oggetto application nel file AndroidManifest.xml, che si trova in app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. Quindi, crea un nuovo file di layout denominato activity_main.xml nella directory app/src/main/res/layout/ e definiscilo nel seguente modo:

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>

Questo layout ha un solo FrameLayout contenente un SupportMapFragment. Questo frammento contiene l'oggetto GoogleMaps sottostante che utilizzerai nei passaggi successivi.

  1. Infine, aggiorna la classe MainActivity che si trova in app/src/main/java/com/google/codelabs/buildyourfirstmap aggiungendo il seguente codice per eseguire l'override del metodo onCreate in modo da poter impostare i relativi contenuti con il nuovo layout appena creato.

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Ora esegui l'app. Dovresti vedere la mappa caricata sullo schermo del dispositivo.

5. Personalizzazione delle mappe basata su cloud (facoltativo)

Puoi personalizzare lo stile della mappa utilizzando la personalizzazione delle mappe basata su cloud.

Creare un ID mappa

Se non hai ancora creato un ID mappa a cui è associato uno stile di mappa, consulta la guida ID mappa per completare i seguenti passaggi:

  1. Crea un ID mappa.
  2. Associa un ID mappa a uno stile di mappa.

Aggiungere l'ID mappa alla tua app

Per utilizzare l'ID mappa che hai creato, modifica il file activity_main.xml e trasmetti l'ID mappa nell'attributo map:mapId di 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" />

Una volta completata questa operazione, esegui l'app per visualizzare la mappa nello stile che hai selezionato.

6. Aggiungi indicatori

In questa attività, aggiungi alla mappa indicatori che rappresentano i punti di interesse che vuoi evidenziare. Per prima cosa, recupera un elenco di luoghi forniti nel progetto iniziale, poi aggiungili alla mappa. In questo esempio, si tratta di negozi di biciclette.

bc5576877369b554.png

Ottenere un riferimento a GoogleMap

Innanzitutto, devi ottenere un riferimento all'oggetto GoogleMap per poter utilizzare i relativi metodi. Per farlo, aggiungi il seguente codice nel metodo MainActivity.onCreate() subito dopo la chiamata a setContentView():

MainActivity.onCreate()

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

L'implementazione trova innanzitutto l'SupportMapFragment che hai aggiunto nel passaggio precedente utilizzando il metodo findFragmentById() sull'oggetto SupportFragmentManager. Una volta ottenuto un riferimento, viene richiamata la chiamata getMapAsync(), seguita dal passaggio di una lambda. In questa espressione lambda viene passato l'oggetto GoogleMap. All'interno di questa lambda viene richiamata la chiamata al metodo addMarkers(), che verrà definita a breve.

Classe fornita: PlacesReader

Nel progetto iniziale, la classe PlacesReader è già stata fornita. Questa classe legge un elenco di 49 luoghi memorizzati in un file JSON denominato places.json e li restituisce come List<Place>. I luoghi stessi rappresentano un elenco di negozi di biciclette nei dintorni di San Francisco, California, Stati Uniti.

Se vuoi saperne di più sull'implementazione di questa classe, puoi accedervi su GitHub o aprire la classe PlacesReader in 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()
       }
   }

Caricare i luoghi

Per caricare l'elenco dei negozi di biciclette, aggiungi una proprietà in MainActivity chiamata places e definiscila nel seguente modo:

MainActivity.places

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

Questo codice richiama il metodo read() su un PlacesReader, che restituisce un List<Place>. Un Place ha una proprietà chiamata name, il nome del luogo e un latLng, ovvero le coordinate in cui si trova il luogo.

Luogo

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

Aggiungere indicatori alla mappa

Ora che l'elenco dei luoghi è stato caricato in memoria, il passaggio successivo consiste nel rappresentarli sulla mappa.

  1. Crea un metodo in MainActivity denominato addMarkers() e definiscilo nel seguente modo:

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)
       )
   }
}

Questo metodo esegue l'iterazione nell'elenco di places e poi richiama il metodo addMarker() sull'oggetto GoogleMap fornito. Il marcatore viene creato istanziando un oggetto MarkerOptions, che ti consente di personalizzarlo. In questo caso, vengono forniti il titolo e la posizione del marcatore, che rappresentano rispettivamente il nome del negozio di biciclette e le relative coordinate.

  1. Esegui l'app e vai a San Francisco per vedere i segnaposto che hai appena aggiunto.

7. Personalizzare gli indicatori

Esistono diverse opzioni di personalizzazione per i segnaposto che hai appena aggiunto, per metterli in evidenza e fornire informazioni utili agli utenti. In questa attività, esplorerai alcune di queste funzionalità personalizzando l'immagine di ogni indicatore e la finestra informativa visualizzata quando viene toccato un indicatore.

a26f82802fe838e9.png

Aggiungere una finestra informativa

Per impostazione predefinita, la finestra informativa quando tocchi un indicatore mostra il titolo e lo snippet (se impostati). Puoi personalizzarlo in modo che mostri informazioni aggiuntive, come l'indirizzo e la valutazione del luogo.

Crea marker_info_contents.xml

Innanzitutto, crea un nuovo file di layout denominato marker_info_contents.xml.

  1. Per farlo, fai clic con il tasto destro del mouse sulla cartella app/src/main/res/layout nella visualizzazione del progetto in Android Studio e seleziona Nuovo > File di risorse di layout.

8cac51fcbef9171b.png

  1. Nella finestra di dialogo, digita marker_info_contents nel campo Nome file e LinearLayout nel campo Root element, quindi fai clic su Ok.

8783af12baf07a80.png

Questo file di layout viene poi espanso per rappresentare i contenuti all'interno della finestra informativa.

  1. Copia i contenuti del seguente snippet di codice, che aggiunge tre TextViews all'interno di un gruppo di visualizzazione verticale LinearLayout, e sovrascrivi il codice predefinito nel file.

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>

Crea un'implementazione di un InfoWindowAdapter

Dopo aver creato il file di layout per la finestra informativa personalizzata, il passaggio successivo consiste nell'implementare l'interfaccia GoogleMap.InfoWindowAdapter. Questa interfaccia contiene due metodi: getInfoWindow() e getInfoContents(). Entrambi i metodi restituiscono un oggetto View facoltativo, in cui il primo viene utilizzato per personalizzare la finestra stessa, mentre il secondo per personalizzarne i contenuti. Nel tuo caso, implementi entrambi e personalizzi il ritorno di getInfoContents() restituendo null in getInfoWindow(), il che indica che deve essere utilizzata la finestra predefinita.

  1. Crea un nuovo file Kotlin denominato MarkerInfoWindowAdapter nello stesso pacchetto di MainActivity facendo clic con il tasto destro del mouse sulla cartella app/src/main/java/com/google/codelabs/buildyourfirstmap nella visualizzazione del progetto in Android Studio, quindi seleziona Nuovo > File/classe Kotlin.

3975ba36eba9f8e1.png

  1. Nella finestra di dialogo, digita MarkerInfoWindowAdapter e mantieni evidenziato File.

992235af53d3897f.png

  1. Una volta creato il file, copia i contenuti del seguente snippet di codice nel nuovo file.

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
   }
}

Nei contenuti del metodo getInfoContents(), il marcatore fornito nel metodo viene convertito in un tipo Place e, se la conversione non è possibile, il metodo restituisce null (non hai ancora impostato la proprietà tag su Marker, ma lo farai nel passaggio successivo).

Successivamente, viene visualizzato il layout marker_info_contents.xml e il testo sul contenitore TextViews viene impostato sul tag Place.

Aggiorna MainActivity

Per unire tutti i componenti che hai creato finora, devi aggiungere due righe alla classe MainActivity.

Innanzitutto, per passare InfoWindowAdapter e MarkerInfoWindowAdapter personalizzati all'interno della chiamata al metodo getMapAsync, richiama il metodo setInfoWindowAdapter() sull'oggetto GoogleMap e crea una nuova istanza di MarkerInfoWindowAdapter.

  1. Aggiungi il seguente codice dopo la chiamata al metodo addMarkers() all'interno della lambda getMapAsync().

MainActivity.onCreate()

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

Infine, devi impostare ogni luogo come proprietà del tag su ogni indicatore aggiunto alla mappa.

  1. Per farlo, modifica la chiamata places.forEach{} nella funzione addMarkers() con quanto segue:

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
}

Aggiungere un'immagine di marcatore personalizzata

La personalizzazione dell'immagine dell'indicatore è uno dei modi divertenti per comunicare il tipo di luogo che l'indicatore rappresenta sulla mappa. Per questo passaggio, visualizza le biciclette anziché i segnaposto rossi predefiniti per rappresentare ogni negozio sulla mappa. Il progetto iniziale include l'icona della bicicletta ic_directions_bike_black_24dp.xml in app/src/res/drawable, che utilizzi.

6eb7358bb61b0a88.png

Impostare una bitmap personalizzata sul marcatore

Con l'icona della bicicletta in formato vector drawable a tua disposizione, il passaggio successivo consiste nell'impostare questo elemento disegnabile come icona di ogni indicatore sulla mappa. MarkerOptions ha un metodo icon, che accetta un BitmapDescriptor che utilizzi per raggiungere questo obiettivo.

Innanzitutto, devi convertire la risorsa disegnabile vettoriale che hai appena aggiunto in un BitmapDescriptor. Un file denominato BitMapHelper incluso nel progetto iniziale contiene una funzione di assistenza denominata vectorToBitmap(), che fa proprio questo.

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)
   }
}

Questo metodo accetta un Context, un ID risorsa disegnabile e un numero intero che rappresenta un colore e crea una rappresentazione BitmapDescriptor.

Utilizzando il metodo helper, dichiara una nuova proprietà chiamata bicycleIcon e assegnale la seguente definizione: MainActivity.bicycleIcon

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)
}

Questa proprietà utilizza il colore predefinito colorPrimary nella tua app e lo utilizza per colorare l'icona della bicicletta e restituirla come BitmapDescriptor.

  1. Utilizzando questa proprietà, richiama il metodo icon di MarkerOptions nel metodo addMarkers() per completare la personalizzazione dell'icona. In questo modo, la proprietà del marcatore dovrebbe avere il seguente aspetto:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Esegui l'app per visualizzare i marcatori aggiornati.

8. Indicatori del cluster

A seconda del livello di zoom sulla mappa, potresti aver notato che i marcatori che hai aggiunto si sovrappongono. I marcatori sovrapposti sono molto difficili da usare e creano molto rumore, il che influisce sull'usabilità della tua app.

68591edc86d73724.png

Per migliorare l'esperienza utente, quando hai un set di dati di grandi dimensioni raggruppati in modo ravvicinato, la best practice è implementare il clustering dei marcatori. Con il clustering, man mano che aumenti o diminuisci lo zoom della mappa, i segnaposto che si trovano nelle vicinanze vengono raggruppati in questo modo:

f05e1ca27ff42bf6.png

Per implementare questa funzionalità, devi utilizzare la libreria di utilità di Maps SDK for Android.

Libreria di utilità di Maps SDK for Android

La libreria di utilità di Maps SDK for Android è stata creata per estendere la funzionalità di Maps SDK for Android. Offre funzionalità avanzate, come il clustering dei marcatori, le mappe termiche, il supporto di KML e GeoJSON, la codifica e la decodifica delle polilinee e una serie di funzioni di assistenza per la geometria sferica.

Aggiorna il file build.gradle

Poiché la libreria di utilità è confezionata separatamente da Maps SDK for Android, devi aggiungere una dipendenza aggiuntiva al file build.gradle.

  1. Aggiorna la sezione dependencies del file app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Dopo aver aggiunto questa riga, devi eseguire una sincronizzazione del progetto per recuperare le nuove dipendenze.

b7b030ec82c007fd.png

Implementare il clustering

Per implementare il clustering nella tua app, segui questi tre passaggi:

  1. Implementa l'interfaccia ClusterItem.
  2. Crea una sottoclasse della classe DefaultClusterRenderer.
  3. Crea un ClusterManager e aggiungi elementi.

Implementa l'interfaccia ClusterItem

Tutti gli oggetti che rappresentano un indicatore raggruppabile sulla mappa devono implementare l'interfaccia ClusterItem. Nel tuo caso, significa che il modello Place deve essere conforme a ClusterItem. Apri il file Place.kt e apporta le seguenti modifiche:

Luogo

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
}

ClusterItem definisce questi tre metodi:

  • getPosition(), che rappresenta il LatLng del luogo.
  • getTitle(), che rappresenta il nome del luogo
  • getSnippet(), che rappresenta l'indirizzo del luogo.

Esegui la sottoclasse della classe DefaultClusterRenderer

La classe responsabile dell'implementazione del clustering, ClusterManager, utilizza internamente una classe ClusterRenderer per gestire la creazione dei cluster mentre sposti e aumenti lo zoom sulla mappa. Per impostazione predefinita, viene fornito con il renderer predefinito, DefaultClusterRenderer, che implementa ClusterRenderer. Per i casi semplici, questo dovrebbe essere sufficiente. Nel tuo caso, tuttavia, poiché i marcatori devono essere personalizzati, devi estendere questa classe e aggiungere le personalizzazioni.

Crea il file Kotlin PlaceRenderer.kt nel pacchetto com.google.codelabs.buildyourfirstmap.place e definiscilo come segue:

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
   }
}

Questa classe esegue l'override di queste due funzioni:

  • onBeforeClusterItemRendered(), che viene chiamato prima che il cluster venga visualizzato sulla mappa. Qui puoi fornire personalizzazioni tramite MarkerOptions. In questo caso, vengono impostati il titolo, la posizione e l'icona del marcatore.
  • onClusterItemRenderer(), che viene chiamato subito dopo il rendering del marcatore sulla mappa. Qui puoi accedere all'oggetto Marker creato, che in questo caso imposta la proprietà tag del marcatore.

Crea un ClusterManager e aggiungi elementi

Infine, per far funzionare il clustering, devi modificare MainActivity per creare un'istanza di ClusterManager e fornire le dipendenze necessarie. ClusterManager gestisce internamente l'aggiunta dei marcatori (gli oggetti ClusterItem), quindi invece di aggiungere i marcatori direttamente sulla mappa, questa responsabilità viene delegata a ClusterManager. Inoltre, ClusterManager chiama internamente anche setInfoWindowAdapter(), quindi l'impostazione di una finestra informativa personalizzata dovrà essere eseguita sull'oggetto MarkerManager.Collection di ClusterManger.

  1. Per iniziare, modifica i contenuti della lambda nella chiamata getMapAsync() in MainActivity.onCreate(). Commenta la chiamata a addMarkers() e setInfoWindowAdapter() e richiama invece un metodo denominato addClusteredMarkers(), che definirai in seguito.

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Poi, in MainActivity, definisci 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()
   }
}

Questo metodo crea un'istanza di ClusterManager, gli trasmette il renderer personalizzato PlacesRenderer, aggiunge tutti i luoghi e richiama il metodo cluster(). Inoltre, poiché ClusterManager utilizza il metodo setInfoWindowAdapter() sull'oggetto mappa, l'impostazione della finestra informativa personalizzata dovrà essere eseguita sull'oggetto ClusterManager.markerCollection. Infine, poiché vuoi che il clustering cambi man mano che l'utente esegue panoramiche e zoom sulla mappa, viene fornito un OnCameraIdleListener a googleMap, in modo che quando la videocamera è inattiva, venga richiamato clusterManager.onCameraIdle().

  1. Esegui l'app per visualizzare i nuovi negozi raggruppati.

9. Disegna sulla mappa

Anche se hai già esplorato un modo per disegnare sulla mappa (aggiungendo indicatori), Maps SDK for Android supporta molti altri modi per disegnare e visualizzare informazioni utili sulla mappa.

Ad esempio, se vuoi rappresentare percorsi e aree sulla mappa, puoi utilizzare polilinee e poligoni per visualizzarli sulla mappa. In alternativa, se vuoi fissare un'immagine alla superficie del terreno, puoi utilizzare gli overlay a terra.

In questa attività, imparerai a disegnare forme, in particolare un cerchio, intorno a un indicatore ogni volta che viene toccato.

f98ce13055430352.png

Aggiungere un listener di clic

In genere, per aggiungere un listener di clic a un indicatore, devi trasmettere un listener di clic direttamente all'oggetto GoogleMap tramite setOnMarkerClickListener(). Tuttavia, poiché utilizzi il clustering, il listener di clic deve essere fornito a ClusterManager.

  1. Nel metodo addClusteredMarkers() in MainActivity, aggiungi la seguente riga subito dopo la chiamata a cluster().

MainActivity.addClusteredMarkers()

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

Questo metodo aggiunge un listener e richiama il metodo addCircle(), che definirai in seguito. Infine, questo metodo restituisce false per indicare che non ha utilizzato questo evento.

  1. Successivamente, devi definire la proprietà circle e il metodo addCircle() in 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 proprietà circle è impostata in modo che ogni volta che viene toccato un nuovo indicatore, il cerchio precedente venga rimosso e ne venga aggiunto uno nuovo. Tieni presente che l'API per l'aggiunta di un cerchio è molto simile a quella per l'aggiunta di un indicatore.

  1. Ora esegui l'app per visualizzare le modifiche.

10. Controllo della videocamera

Come ultima attività, dai un'occhiata ai controlli della videocamera per mettere a fuoco la visualizzazione su una determinata regione.

Fotocamera e visualizzazione

Se hai notato che quando esegui l'app, la videocamera mostra il continente africano e devi spostare e ingrandire faticosamente l'immagine fino a San Francisco per trovare i segnaposto che hai aggiunto. Anche se può essere un modo divertente per esplorare il mondo, non è utile se vuoi visualizzare subito i segnaposto.

Per facilitare questa operazione, puoi impostare la posizione della videocamera a livello di programmazione in modo che la visuale sia centrata sul punto che ti interessa.

  1. Aggiungi il seguente codice alla chiamata getMapAsync() per regolare la visualizzazione della videocamera in modo che venga inizializzata su San Francisco all'avvio dell'app.

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))
   }
}

Innanzitutto, viene chiamato setOnMapLoadedCallback() in modo che l'aggiornamento della videocamera venga eseguito solo dopo il caricamento della mappa. Questo passaggio è necessario perché le proprietà della mappa, come le dimensioni, devono essere calcolate prima di effettuare una chiamata di aggiornamento della videocamera.

Nella funzione lambda viene costruito un nuovo oggetto LatLngBounds, che definisce una regione rettangolare sulla mappa. Viene creato in modo incrementale includendo tutti i valori LatLng del luogo per garantire che tutti i luoghi si trovino all'interno dei limiti. Una volta creato questo oggetto, viene richiamato il metodo moveCamera() su GoogleMap e viene fornito un CameraUpdate tramite CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

  1. Esegui l'app e nota che la videocamera è ora inizializzata a San Francisco.

Ascolto delle modifiche alla videocamera

Oltre a modificare la posizione della videocamera, puoi anche ascoltare gli aggiornamenti della videocamera mentre l'utente si sposta sulla mappa. Questa operazione può essere utile se vuoi modificare la UI mentre la videocamera si sposta.

Per divertimento, modifichi il codice in modo che i marcatori diventino traslucidi ogni volta che la videocamera viene spostata.

  1. Nel metodo addClusteredMarkers(), aggiungi le seguenti righe verso la fine del metodo:

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 }
}

In questo modo viene aggiunto un OnCameraMoveStartedListener in modo che, ogni volta che la videocamera inizia a muoversi, i valori alfa di tutti i marcatori (cluster e marcatori) vengano modificati in 0.3f in modo che i marcatori appaiano traslucidi.

  1. Infine, per modificare i marcatori traslucidi in modo che tornino opachi quando la videocamera si ferma, modifica i contenuti di setOnCameraIdleListener nel metodo addClusteredMarkers() nel seguente modo:

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. Esegui l'app per visualizzare i risultati.

11. Maps KTX

Per le app Kotlin che utilizzano uno o più SDK Android di Google Maps Platform, sono disponibili librerie di estensioni Kotlin o KTX per consentirti di sfruttare le funzionalità del linguaggio Kotlin come coroutine, proprietà/funzioni di estensione e altro ancora. Ogni SDK Google Maps ha una libreria KTX corrispondente, come mostrato di seguito:

Diagramma KTX di Google Maps Platform

In questa attività, utilizzerai le librerie Maps KTX e Maps Utils KTX nella tua app e refactorizzerai le implementazioni delle attività precedenti in modo da poter utilizzare le funzionalità specifiche del linguaggio Kotlin nella tua app.

  1. Includi le dipendenze KTX nel file build.gradle a livello di app

Poiché l'app utilizza sia Maps SDK for Android sia la libreria di utilità di Maps SDK for Android, dovrai includere le librerie KTX corrispondenti per queste librerie. In questa attività utilizzerai anche una funzionalità presente nella libreria AndroidX Lifecycle KTX, quindi includi anche questa dipendenza nel file build.gradle a livello di app.

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. Utilizzare le funzioni di estensione GoogleMap.addMarker() e GoogleMap.addCircle()

La libreria Maps KTX fornisce un'alternativa all'API in stile DSL per GoogleMap.addMarker(MarkerOptions) e GoogleMap.addCircle(CircleOptions) utilizzati nei passaggi precedenti. Per utilizzare le API menzionate in precedenza, è necessaria la creazione di una classe contenente le opzioni per un indicatore o un cerchio, mentre con le alternative KTX puoi impostare le opzioni per l'indicatore o il cerchio nel lambda che fornisci.

Per utilizzare queste API, aggiorna i metodi MainActivity.addMarkers(GoogleMap) e 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))
    }
}

La riscrittura dei metodi precedenti in questo modo è molto più concisa da leggere, il che è reso possibile dall'espressione funzionale con ricevitore di Kotlin.

  1. Utilizzare le funzioni di sospensione delle estensioni SupportMapFragment.awaitMap() e GoogleMap.awaitMapLoad()

La libreria Maps KTX fornisce anche estensioni di funzioni di sospensione da utilizzare all'interno delle coroutine. Nello specifico, esistono alternative alla funzione di sospensione per SupportMapFragment.getMapAsync(OnMapReadyCallback) e GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). L'utilizzo di queste API alternative elimina la necessità di passare i callback e ti consente invece di ricevere la risposta di questi metodi in modo seriale e sincrono.

Poiché questi metodi sospendono le funzioni, il loro utilizzo dovrà avvenire all'interno di una coroutine. La libreria Lifecycle Runtime KTX offre un'estensione per fornire ambiti di coroutine sensibili al ciclo di vita, in modo che le coroutine vengano eseguite e arrestate in corrispondenza dell'evento del ciclo di vita appropriato.

Combinando questi concetti, aggiorna il metodo 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)
    }
}

L'ambito della coroutine lifecycleScope.launchWhenCreated eseguirà il blocco quando l'attività è almeno nello stato creato. Inoltre, nota che le chiamate per recuperare l'oggetto GoogleMap e per attendere il completamento del caricamento della mappa sono state sostituite rispettivamente da SupportMapFragment.awaitMap() e GoogleMap.awaitMapLoad(). Il refactoring del codice utilizzando queste funzioni di sospensione consente di scrivere il codice equivalente basato su callback in modo sequenziale.

  1. Procedi e ricompila l'app con le modifiche refactoring.

12. Complimenti

Complimenti! Hai esaminato molti contenuti e ci auguriamo che tu abbia una migliore comprensione delle funzionalità principali offerte in Maps SDK for Android.

Scopri di più

  • Places SDK for Android: esplora il ricco set di dati sui luoghi per scoprire le attività commerciali che ti circondano.
  • android-maps-ktx: una libreria open source che consente di integrarsi con Maps SDK for Android e la libreria di utilità di Maps SDK for Android in modo compatibile con Kotlin.
  • android-place-ktx: una libreria open source che ti consente di integrarti con Places SDK for Android in modo compatibile con Kotlin.
  • android-samples: codice campione su GitHub che mostra tutte le funzionalità trattate in questo codelab e altro ancora.
  • Altri codelab Kotlin per la creazione di app per Android con Google Maps Platform