Informazioni su questo codelab
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.
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
- Maps SDK for Android
- Un Account Google con la fatturazione abilitata
- Android Studio 2020.3.1 o versioni successive
- Google Play Services installato in Android Studio
- Un dispositivo Android o un emulatore Android che esegue la piattaforma Google APIs basata su Android 4.2.2 o versioni successive (consulta Eseguire app sull'emulatore Android per i passaggi di installazione).
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.
- Nella console Cloud, fai clic sul menu a discesa del progetto e seleziona il progetto che vuoi utilizzare per questo codelab.
- 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.
- 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.
- 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.
- 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.
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.
- Per fornirlo, apri il file denominato
local.properties
nella directory principale del progetto (lo stesso livello digradle.properties
esettings.gradle
). - 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.
- 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 directoryapp/
e aggiungi la seguente riga all'interno del bloccoplugins
:
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
- 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'
}
- Successivamente, aggiungi un nuovo tag
meta-data
inAndroidManifest.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 tagmeta-data
all'interno dell'oggettoapplication
nel fileAndroidManifest.xml
, che si trova inapp/src/main
.
AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- Quindi, crea un nuovo file di layout denominato
activity_main.xml
nella directoryapp/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.
- Infine, aggiorna la classe
MainActivity
che si trova inapp/src/main/java/com/google/codelabs/buildyourfirstmap
aggiungendo il seguente codice per eseguire l'override del metodoonCreate
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)
}
- 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:
- Crea un ID mappa.
- 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.
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.
- Crea un metodo in
MainActivity
denominatoaddMarkers()
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.
- 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.
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
.
- 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.
- Nella finestra di dialogo, digita
marker_info_contents
nel campo Nome file eLinearLayout
nel campoRoot element
, quindi fai clic su Ok.
Questo file di layout viene poi espanso per rappresentare i contenuti all'interno della finestra informativa.
- Copia i contenuti del seguente snippet di codice, che aggiunge tre
TextViews
all'interno di un gruppo di visualizzazione verticaleLinearLayout
, 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.
- Crea un nuovo file Kotlin denominato
MarkerInfoWindowAdapter
nello stesso pacchetto diMainActivity
facendo clic con il tasto destro del mouse sulla cartellaapp/src/main/java/com/google/codelabs/buildyourfirstmap
nella visualizzazione del progetto in Android Studio, quindi seleziona Nuovo > File/classe Kotlin.
- Nella finestra di dialogo, digita
MarkerInfoWindowAdapter
e mantieni evidenziato File.
- 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
.
- Aggiungi il seguente codice dopo la chiamata al metodo
addMarkers()
all'interno della lambdagetMapAsync()
.
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.
- Per farlo, modifica la chiamata
places.forEach{}
nella funzioneaddMarkers()
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.
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
.
- Utilizzando questa proprietà, richiama il metodo
icon
diMarkerOptions
nel metodoaddMarkers()
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)
)
- 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.
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:
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
.
- Aggiorna la sezione
dependencies
del fileapp/build.gradle
.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- Dopo aver aggiunto questa riga, devi eseguire una sincronizzazione del progetto per recuperare le nuove dipendenze.
Implementare il clustering
Per implementare il clustering nella tua app, segui questi tre passaggi:
- Implementa l'interfaccia
ClusterItem
. - Crea una sottoclasse della classe
DefaultClusterRenderer
. - 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 ilLatLng
del luogo.getTitle()
, che rappresenta il nome del luogogetSnippet()
, 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 tramiteMarkerOptions
. 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'oggettoMarker
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
.
- Per iniziare, modifica i contenuti della lambda nella chiamata
getMapAsync()
inMainActivity.onCreate()
. Commenta la chiamata aaddMarkers()
esetInfoWindowAdapter()
e richiama invece un metodo denominatoaddClusteredMarkers()
, che definirai in seguito.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- Poi, in
MainActivity
, definisciaddClusteredMarkers()
.
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()
.
- 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.
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
.
- Nel metodo
addClusteredMarkers()
inMainActivity
, aggiungi la seguente riga subito dopo la chiamata acluster()
.
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.
- Successivamente, devi definire la proprietà
circle
e il metodoaddCircle()
inMainActivity
.
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.
- 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.
- 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)
.
- 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.
- 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.
- Infine, per modificare i marcatori traslucidi in modo che tornino opachi quando la videocamera si ferma, modifica i contenuti di
setOnCameraIdleListener
nel metodoaddClusteredMarkers()
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()
}
- 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:
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.
- 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'
}
- 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.
- 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.
- 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