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.
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
- SDK Maps per Android
- Un Account Google con 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 API di Google in base ad Android 4.2.2 o versioni successive (per la procedura di installazione, consulta l'articolo Eseguire app sull'emulatore Android).
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.
- In Cloud Console, 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 la procedura descritta in questo video o in questa documentazione.
- 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.
- 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.
- 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.
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.
- Per farlo, apri il file denominato
local.properties
nella directory radice del progetto (lo stesso livello in cui si trovanogradle.properties
esettings.gradle
). - 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.
- 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 directoryapp/
e aggiungi la seguente riga nel bloccoplugins
:
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
- 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'
}
- Aggiungi quindi un nuovo tag
meta-data
inAndroidManifest.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 tagmeta-data
all'interno dell'oggettoapplication
nel fileAndroidManifest.xml
, situato 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 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.
- Infine, aggiorna la classe
MainActivity
che si trova inapp/src/main/java/com/google/codelabs/buildyourfirstmap
aggiungendo il seguente codice per sostituire il metodoonCreate
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)
}
- 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:
- Crea un ID mappa.
- 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.
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.
- Crea un metodo in
addMarkers()
denominatoMainActivity
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.
- 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.
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
.
- 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.
- Nella finestra di dialogo, digita
marker_info_contents
nel campo File name (Nome file) eLinearLayout
nel campoRoot element
, quindi fai clic su OK.
che viene successivamente aumentato in modo da rappresentare i contenuti all'interno della finestra informativa.
- Copia i contenuti nel seguente snippet di codice, che aggiunge tre
TextViews
in un gruppo di vista verticaleLinearLayout
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.
- Crea un nuovo file Kotlin denominato
MarkerInfoWindowAdapter
nello stesso pacchetto diMainActivity
facendo clic con il pulsante 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 tieni evidenziato il file File.
- 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
.
- Per farlo, aggiungi il seguente codice dopo la chiamata al metodo
addMarkers()
all'interno del parametro 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()
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.
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
.
- Utilizzando questa proprietà, prosegui e richiama il metodo
icon
diMarkerOptions
nel metodoaddMarkers()
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)
)
- 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.
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:
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
.
- 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 nell'app, segui questi tre passaggi:
- Implementa l'interfaccia di
ClusterItem
. - Sottoclasse la classe
DefaultClusterRenderer
. - 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 rappresentaLatLng
del luogo.getTitle()
, che rappresenta il nome del luogogetSnippet()
, 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 tramiteMarkerOptions
: 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'oggettoMarker
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
.
- Per iniziare, modifica il contenuto del lambda nella chiamata a
getMapAsync()
inMainActivity.onCreate()
. Continua e commenta la chiamata aaddMarkers()
esetInfoWindowAdapter()
, quindi richiama un metodo chiamatoaddClusteredMarkers()
come spiegato di seguito.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- Successivamente, 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
, 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()
.
- 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.
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
.
- Nel metodo
addClusteredMarkers()
inMainActivity
, prosegui e aggiungi la riga seguente 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 definisci successivamente. Infine, false
restituisce un metodo che indica che il metodo non ha utilizzato questo evento.
- Poi, 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 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.
- 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.
- 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)
.
- 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.
- 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.
- Infine, per ripristinare l'opaca degli indicatori traslucidi quando la fotocamera si interrompe, modifica i contenuti di
setOnCameraIdleListener
nel metodoaddClusteredMarkers()
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()
}
- 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:
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.
- 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'
}
- 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.
- 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.
- 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