Aggiungere una mappa all'app Android (Kotlin)

1. Prima di iniziare

Questo codelab ti insegna come 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, California, Stati Uniti.

f05e1ca27ff42bf6.png

Prerequisiti

  • Conoscenza di base dello sviluppo di Kotlin e Android

In questo lab proverai a:

  • Attiva e utilizza Maps SDK for Android per aggiungere Google Maps a un'app Android.
  • Aggiungi, personalizza e raggruppa gli indicatori.
  • Disegna polilinei e poligoni sulla mappa.
  • Controllare il punto di vista della videocamera in modo programmatico.

Che cosa ti serve

2. Configura

Per il seguente passaggio , devi attivare l'SDK Maps per Android.

Configurare Google Maps Platform

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

  1. In Cloud Console, 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 la procedura descritta in questo video o in questa documentazione.
  2. Genera una chiave API nella pagina Credentials di Cloud Console. Puoi seguire la procedura descritta 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 di avvio che ti aiuterà a seguire questo codelab. Ti invitiamo a passare alla soluzione, ma se vuoi seguire tutti i passaggi per crearla da sola, 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. Dopo avere ottenuto il codice, apri il progetto presente nella directory starter di Android Studio.

4. Aggiungi Google Maps

In questa sezione aggiungerai Google Maps, in modo che venga caricato quando avvii l'app.

d1d068b5d4ae38b9.png

Aggiungi la tua chiave API

La chiave API creata in un passaggio precedente deve essere fornita all'app per consentire ad Maps SDK for Android di associarla alla tua app.

  1. Per farlo, apri il file denominato local.properties nella directory radice del progetto (lo stesso livello in cui si trovano gradle.properties e settings.gradle).
  2. In questo file, definisci una nuova chiave GOOGLE_MAPS_API_KEY con il valore corrispondente alla chiave API creata da te.

proprietà.local

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Nota: local.properties è elencato nel file .gitignore nel repository Git. Ciò è dovuto al fatto che la tua chiave API è considerata sensibile e non deve essere controllata al controllo della sorgente, se possibile.

  1. Per esporre l'API in modo che possa essere utilizzata in tutta l'app, includi il plug-in Secret di Gradle per Android nel file build.gradle dell'app situato nella directory app/ e aggiungi la seguente riga nel blocco plugins:

build.gradle a livello di app

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

Dovrai inoltre modificare il file build.gradle a livello di progetto per includere il seguente percorso di classe:

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 definite all'interno del file local.properties come variabili build nel file manifest di Android e come variabili nella classe BuildConfig generata da Gradle al momento della creazione. L'utilizzo di questo plug-in comporta la rimozione del codice boilerplate altrimenti necessario per leggere le proprietà da local.properties, in modo che sia possibile accedervi da tutta l'app.

Aggiungere la dipendenza da Google Maps

  1. Ora che puoi accedere alla tua chiave API all'interno dell'app, il passaggio successivo consiste nell'aggiungere la dipendenza Maps SDK for Android alla tua app build.gradle.

Nel progetto iniziale fornito con questo codelab, questa dipendenza è già stata aggiunta per te.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Aggiungi quindi un nuovo tag meta-data in AndroidManifest.xml per passare la chiave API creata in un passaggio precedente. A tale scopo, prosegui e apri questo file in Android Studio, quindi aggiungi il seguente tag meta-data all'interno dell'oggetto application nel file AndroidManifest.xml, situato 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 come segue:

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 singolo 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 sostituire il metodo onCreate in modo che possa impostare i contenuti con il nuovo layout appena creato.

Attività principale

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Ora procedi ed esegui l'app. Ora dovresti vedere il caricamento della mappa sullo schermo del tuo dispositivo.

5. Stile della mappa basato su cloud (facoltativo)

Puoi personalizzare lo stile della mappa utilizzando gli stili basati su cloud.

Crea un ID mappa

Se non hai ancora creato un ID mappa con uno stile associato, consulta la guida ID mappa per completare la seguente procedura:

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

Aggiungere l'ID mappa alla tua app

Per utilizzare l'ID mappa che hai creato, modifica il file activity_main.xml e trasmetti il tuo 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, procedi ed esegui l'app per visualizzare la mappa nello stile che hai selezionato.

6. Aggiungi indicatori

In questa attività, aggiungi indicatori alla mappa che rappresentano i punti di interesse che vuoi evidenziare sulla mappa. Innanzitutto, recupera un elenco di luoghi che ti sono stati forniti nel progetto iniziale e poi aggiungili alla mappa. In questo esempio, si tratta di negozi di biciclette.

bc5576877369b554.png

Ricevere un riferimento a Google Maps

Innanzitutto, devi ottenere un riferimento all'oggetto GoogleMap per poterne utilizzare i metodi. A tale scopo, aggiungi il seguente codice nel tuo 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 il 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 un lambda. Questo lambda è il punto in cui viene passato l'oggetto GoogleMap. All'interno di questo lambda, viene richiamata la chiamata metodo addMarkers(), definita a breve.

Classe fornita: PlacesReader

Nel progetto iniziale ti è stato fornito il corso PlacesReader. Questo corso legge un elenco di 49 luoghi archiviati in un file JSON denominato places.json e li restituisce come List<Place>. I luoghi stessi rappresentano un elenco di negozi di biciclette intorno a Roma, Italia.

Se vuoi saperne di più sull'implementazione di questo corso, puoi accedervi su GitHub o aprire il corso PlacesReader in Android Studio.

Luoghi lettori

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

Carica luoghi

Per caricare l'elenco dei negozi di biciclette, aggiungi una proprietà in MainActivity denominata places e definiscila come segue:

MainActivity.place

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

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

Luogo

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

Aggiungi indicatori alla mappa

Una volta caricato l'elenco di luoghi in memoria, il passaggio successivo consiste nel rappresentare questi luoghi sulla mappa.

  1. Crea un metodo in addMarkers() denominato MainActivity e definiscilo come segue:

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 un'iterazione tramite l'elenco di places seguito da un richiamo del metodo addMarker() sull'oggetto GoogleMap fornito. L'indicatore viene creato creando un'istanza di un oggetto MarkerOptions che ti consente di personalizzare l'indicatore stesso. In questo caso, vengono forniti il titolo e la posizione dell'indicatore, che rappresentano rispettivamente il nome del negozio di biciclette e le relative coordinate.

  1. Procedi, esegui l'app e vai su San Francisco per vedere gli indicatori che hai appena aggiunto!

7. Personalizza gli indicatori

Esistono diverse opzioni di personalizzazione per gli indicatori che hai appena aggiunto, per distinguerli e trasmettere informazioni utili agli utenti. In questa attività ne esplorerai alcune personalizzando l'immagine di ogni indicatore e la finestra delle informazioni visualizzata quando viene toccato un indicatore.

a26f82802fe838e9.png

Aggiungere una finestra informativa

Per impostazione predefinita, la finestra informativa quando tocchi un indicatore mostra il relativo titolo e lo snippet (se impostato). Personalizzalo in modo che possa visualizzare informazioni aggiuntive, come l'indirizzo e la valutazione del luogo.

Crea indicatori_info_contents.xml

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

  1. Per farlo, fai clic con il pulsante 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 File name (Nome file) e LinearLayout nel campo Root element, quindi fai clic su OK.

8783af12baf07a80.png

che viene successivamente aumentato in modo da rappresentare i contenuti all'interno della finestra informativa.

  1. Copia i contenuti nel seguente snippet di codice, che aggiunge tre TextViews in un gruppo di vista verticale LinearLayout e sovrascrive il codice predefinito nel file.

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

Creare un'implementazione di un InfowindowAdapter

Dopo aver creato il file di layout per la finestra delle informazioni personalizzate, devi implementare l'interfaccia di 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, mentre il secondo è per personalizzare i contenuti. Nel tuo caso, implementi entrambi e personalizzi il reso di getInfoContents() mentre restituisci null in getInfoWindow(), 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 pulsante 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 tieni evidenziato il file File.

992235af53d3897f.png

  1. Dopo aver creato il file, copia i contenuti del seguente snippet 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(), l'indicatore indicato nel metodo è trasmesso a un tipo Place e, se non è possibile trasmettere, il metodo restituisce null (non hai ancora impostato la proprietà del tag su Marker, ma lo fai nel passaggio successivo).

Successivamente, il layout marker_info_contents.xml viene aumentato in modo artificioso seguito dall'impostazione del testo contenente TextViews nel tag Place.

Aggiorna MainActivity

Per incollare tutti i componenti creati finora, devi aggiungere due righe nel corso della lezione MainActivity.

Innanzitutto, per trasmettere il valore InfoWindowAdapter personalizzato, MarkerInfoWindowAdapter, all'interno della chiamata del metodo getMapAsync, richiama il metodo setInfoWindowAdapter() sull'oggetto GoogleMap e crea una nuova istanza di MarkerInfoWindowAdapter.

  1. Per farlo, aggiungi il seguente codice dopo la chiamata al metodo addMarkers() all'interno del parametro 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() come 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
}

Aggiungi un'immagine di un indicatore personalizzato

Personalizzare l'immagine dell'indicatore è uno dei modi divertenti per comunicare il tipo di luogo che l'indicatore rappresenta sulla mappa. Per questo passaggio, devi visualizzare le biciclette invece degli indicatori 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

Imposta bitmap personalizzata sull'indicatore

Con l'icona della bicicletta da disegnare a disposizione, il passaggio successivo consiste nell'impostarla sulla mappa come icona di ciascun indicatore. MarkerOptions utilizza un metodo icon, che prevede un metodo BitmapDescriptor che utilizzi per eseguire questa operazione.

In primo luogo, devi convertire il disegno astratto che hai appena aggiunto in un BitmapDescriptor. Un file denominato BitMapHelper incluso nel progetto iniziale contiene una funzione helper chiamata vectorToBitmap().

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 prevede un Context, un ID risorsa disegnabile, nonché un numero intero colorato, e ne crea una rappresentazione BitmapDescriptor.

Utilizzando il metodo helper, dichiara una nuova proprietà denominata 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 la utilizza per colorare l'icona della bicicletta e restituirla come BitmapDescriptor.

  1. Utilizzando questa proprietà, prosegui e richiama il metodo icon di MarkerOptions nel metodo addMarkers() per completare la personalizzazione dell'icona. In questo modo, la proprietà indicatore 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 gli indicatori aggiornati.

8. Indicatori del cluster

A seconda di quanto hai aumentato lo zoom sulla mappa, potresti aver notato che gli indicatori aggiunti si sovrappongono. Gli indicatori sovrapposti sono molto difficili da interagire e creano molti rumori, il che influisce sull'usabilità dell'app.

68591edc86d73724.png

Per migliorare l'esperienza utente, quando hai un set di dati di grandi dimensioni che si trova in raggruppamento ravvicinato, è buona norma implementare il clustering degli indicatori. Con il clustering, mano a mano che aumenti e diminuisci lo zoom sulla mappa, gli indicatori nelle vicinanze sono raggruppati insieme nel seguente modo:

f05e1ca27ff42bf6.png

Per l'implementazione, devi avere l'aiuto di Maps SDK for Android Utility Library.

Utility 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 clustering di indicatori, mappe termiche, supporto KML e GeoJson, codifica e decodifica in polilinea e una serie di funzioni di supporto intorno alla geometria sferica.

Aggiorna il build.gradle

Poiché la libreria dell'utilità viene pacchettizzata separatamente dall'SDK Maps per Android, devi aggiungere un'ulteriore dipendenza 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 nell'app, segui questi tre passaggi:

  1. Implementa l'interfaccia di ClusterItem.
  2. Sottoclasse la classe DefaultClusterRenderer.
  3. Crea un elemento ClusterManager e aggiungi elementi.

Implementare l'interfaccia ClusterItem

Tutti gli oggetti che rappresentano un indicatore cluster sulla mappa devono implementare l'interfaccia ClusterItem. In questo caso, il modello Place deve essere conforme a ClusterItem. Procedi e apri il file Place.kt, quindi 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
}

Il ClusterItem definisce questi tre metodi:

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

Sottoclasse la classe DefaultClusterRenderer

La classe responsabile dell'implementazione dei cluster, ClusterManager, utilizza internamente una classe ClusterRenderer per gestire la creazione dei cluster durante la panoramica e lo zoom sulla mappa. Per impostazione predefinita, viene fornito con un renderer predefinito, DefaultClusterRenderer, che implementa ClusterRenderer. Questo dovrebbe essere sufficiente per casi semplici. Nel tuo caso, tuttavia, dato che gli indicatori devono essere personalizzati, dovrai estendere questo corso e aggiungere le personalizzazioni al suo interno.

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

Questo corso sostituisce le due funzioni seguenti:

  • onBeforeClusterItemRendered(), che viene chiamato prima che il cluster venga visualizzato sulla mappa. Qui puoi fornire personalizzazioni tramite MarkerOptions: in questo caso, imposta il titolo, la posizione e l'icona dell'indicatore.
  • onClusterItemRenderer(), che viene chiamato subito dopo che l'indicatore è stato visualizzato sulla mappa. Da qui puoi accedere all'oggetto Marker creato: in questo caso, imposta la proprietà del tag dell'indicatore.

Creare un ClusterManager e aggiungere 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 degli indicatori (gli oggetti ClusterItem). Pertanto, questa responsabilità viene delegata a ClusterManager anziché essere aggiunta direttamente sulla mappa. Inoltre, ClusterManager chiama anche setInfoWindowAdapter() internamente, pertanto l'impostazione di una finestra informativa personalizzata dovrà essere eseguita sull'oggetto MarkerManager.Collection di ClusterManger.

  1. Per iniziare, modifica il contenuto del lambda nella chiamata a getMapAsync() in MainActivity.onCreate(). Continua e commenta la chiamata a addMarkers() e setInfoWindowAdapter(), quindi richiama un metodo chiamato addClusteredMarkers() come spiegato di seguito.

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Successivamente, 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, vi trasmette il renderer personalizzato PlacesRenderer, aggiunge tutti i luoghi e richiama il metodo cluster(). Inoltre, poiché ClusterManager usa il metodo setInfoWindowAdapter() nell'oggetto mappa, sarà necessario impostare la finestra informativa personalizzata sull'oggetto ClusterManager.markerCollection. Infine, dato che vuoi che il clustering cambi quando l'utente esegue una panoramica e uno zoom sulla mappa, viene fornito un valore OnCameraIdleListener a googleMap, per cui quando la fotocamera va in pausa, viene richiamato clusterManager.onCameraIdle().

  1. Esegui l'app per visualizzare i nuovi negozi in cluster.

9. Disegna sulla mappa

Anche se hai già esplorato un modo per disegnare sulla mappa (aggiungendo indicatori), il SDK di Maps per Android supporta numerosi altri modi per disegnare informazioni utili alla visualizzazione.

Ad esempio, se vuoi rappresentare percorsi e aree sulla mappa, puoi utilizzare polilinee e poligoni per visualizzarli. In alternativa, se vuoi correggere un'immagine sulla superficie del suolo, puoi utilizzare gli overlay del suolo.

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

F98ce13055430352.png

Aggiungi listener di clic

In genere, il modo in cui si aggiunge un listener di clic a un indicatore consiste nel trasmettere un listener di clic direttamente sull'oggetto GoogleMap tramite setOnMarkerClickListener(). Tuttavia, poiché utilizzi il clustering, è necessario fornire il listener di clic a ClusterManager.

  1. Nel metodo addClusteredMarkers() in MainActivity, prosegui e aggiungi la riga seguente 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 definisci successivamente. Infine, false restituisce un metodo che indica che il metodo non ha utilizzato questo evento.

  1. Poi, 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 viene rimosso e ne viene aggiunto uno nuovo. Nota che l'API per l'aggiunta di un cerchio è abbastanza simile a quella per l'aggiunta di un indicatore.

  1. Esegui subito l'operazione ed esegui l'app per vedere le modifiche.

10. Controllo fotocamera

Come ultima attività, dai un'occhiata ad alcuni controlli della fotocamera per concentrare l'inquadratura su una determinata area geografica.

Videocamera e vista

Se hai notato che quando esegui l'app, la fotocamera mostra il continente africano e devi spostarti e ingrandire con lo zoom fino a San Francisco per trovare gli indicatori che hai aggiunto. Può essere un modo divertente per esplorare il mondo, ma non è utile per visualizzare subito gli indicatori.

Per aiutarti, puoi impostare la posizione della videocamera in modo programmatico in modo che la vista sia centrata nel punto desiderato.

  1. Aggiungi il seguente codice alla chiamata getMapAsync() per modificare la visualizzazione della fotocamera in modo che venga inizializzata a 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, setOnMapLoadedCallback() viene chiamato in modo che l'aggiornamento della fotocamera venga eseguito solo dopo che la mappa è stata caricata. Questo passaggio è necessario perché le proprietà della mappa, ad esempio le dimensioni, devono essere calcolate prima di effettuare una chiamata di aggiornamento della fotocamera.

Nel lambda, viene creato un nuovo oggetto LatLngBounds che definisce una regione rettangolare sulla mappa. Viene creata in modo incrementale includendo tutti i valori relativi al posizionamento LatLng per garantire che tutti i luoghi rientrino nei 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 fotocamera è ora inizializzata a San Francisco.

Ascoltare i cambiamenti della videocamera

Oltre a modificare la posizione della fotocamera, puoi anche ascoltare gli aggiornamenti della fotocamera mentre l'utente si sposta nella mappa. Questo può essere utile se vuoi modificare l'interfaccia utente mentre la videocamera si muove.

Per divertimento, puoi modificare il codice per rendere gli indicatori trasparenti trascurabili ogni volta che la fotocamera 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 }
}

L'operazione aggiunge un OnCameraMoveStartedListener in modo che, ogni volta che la fotocamera inizia a muoversi, tutti i valori alfa dei marcatori (sia cluster che indicatori) vengano modificati in 0.3f in modo che gli indicatori sembrino trasparenti.

  1. Infine, per ripristinare l'opaca degli indicatori traslucidi quando la fotocamera si interrompe, modifica i contenuti di setOnCameraIdleListener nel metodo addClusteredMarkers() procedendo 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. Vai avanti ed esegui l'app per vedere i risultati.

11. KTX su Maps

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

Diagramma KTX di Google Maps Platform

In questa attività utilizzerai le librerie KTX di Maps e Maps Utils KTX alla tua app e reintegrerai le attività precedenti, in modo da poter utilizzare nell'app le funzionalità linguistiche specifiche di Kotlin.

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

Poiché l'app utilizza sia l'SDK di Maps per Android sia l'SDK di Maps per Android Utility Library, dovrai includere le librerie KTX corrispondenti per queste librerie. In questa attività utilizzerai anche una funzionalità presente nella libreria KTX del ciclo di vita di AndroidX, quindi includi questa dipendenza anche 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 delle estensioni GoogleMap.addMarker() e GoogleMap.addCircle()

La libreria KTX di Maps fornisce un'alternativa all'API in stile DSL per GoogleMap.addMarker(MarkerOptions) e GoogleMap.addCircle(CircleOptions) utilizzati nei passaggi precedenti. Per utilizzare le API sopra menzionate, è necessario creare una classe che contenga le opzioni di un indicatore o di un cerchio, mentre con le alternative KTX puoi impostare le opzioni degli indicatori o dei cerchi 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))
    }
}

Riscrivere i metodi sopra menzionati in questo modo è molto più conciso da leggere, il che è possibile grazie all'utilizzo della funzione letterale con ricevitore di Kotlin.

  1. Usare le funzioni di sospensione dell'estensione SupportMapFragment.awaitMap() e GoogleMap.awaitMapLoad()

La libreria KTX di Maps fornisce anche la sospensione di estensioni funzione da utilizzare all'interno delle coroutine. Nello specifico, esistono delle alternative alla funzione 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 sospenderanno le funzioni, il loro utilizzo dovrà avvenire all'interno di una coroutine. La libreria Lifecycle Runtime KTX offre un'estensione per fornire ambiti coroutine sensibili al ciclo di vita in modo che le coroutine vengano eseguite e arrestate in base all'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 coroutine lifecycleScope.launchWhenCreated eseguirà il blocco quando l'attività è almeno nello stato creato. Tieni inoltre presente che le chiamate per recuperare l'oggetto GoogleMap e in attesa del completamento del caricamento della mappa sono state sostituite rispettivamente con 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 ricostruisci l'app con le modifiche apportate.

12. Complimenti

Complimenti! Hai trattato molti contenuti e speriamo di aver compreso meglio le funzionalità principali offerte nell'SDK di Maps per Android.

Scopri di più

  • SDK Places per Android: esplora il ricco set di dati relativi ai luoghi per scoprire le attività commerciali nelle tue vicinanze.
  • android-maps-ktx: una libreria open source che ti consente di integrarlo con Maps SDK for Android e la Maps SDK for Android Utility Library in modo ottimizzato per Kotlin.
  • android-place-ktx: una libreria open source che ti consente di eseguire l'integrazione con Places SDK for Android in modo compatibile con Kotlin.
  • android-samples: codice di esempio su GitHub che mostra tutte le funzionalità trattate in questo codelab e altro ancora.
  • Altri codelab di Kotlin per creare app Android con Google Maps Platform