1. Прежде чем начать
В этой лабораторной работе вы узнаете, как интегрировать Maps SDK для Android с вашим приложением и использовать его основные функции, создавая приложение, отображающее карту велосипедных магазинов в Сан-Франциско, Калифорния, США.
Предпосылки
- Базовые знания Kotlin и Android-разработки
Что ты будешь делать
- Включите и используйте Maps SDK для Android, чтобы добавить Карты Google в приложение Android.
- Добавляйте, настраивайте и группируйте маркеры.
- Нарисуйте полилинии и многоугольники на карте.
- Управляйте точкой обзора камеры программно.
Что вам понадобится
- Карты SDK для Android
- Аккаунт Google с включенным платежным аккаунтом
- Android Studio 2020.3.1 или выше
- Службы Google Play, установленные в Android Studio
- Устройство Android или эмулятор Android , на котором работает платформа API Google на базе Android 4.2.2 или более поздней версии (этапы установки см. в разделе Запуск приложений на эмуляторе Android ).
2. Настройте
Для следующего шага активации необходимо включить Maps SDK для Android .
Настройте платформу Google Карт
Если у вас еще нет учетной записи Google Cloud Platform и проекта с включенным выставлением счетов, ознакомьтесь с руководством по началу работы с платформой Google Maps , чтобы создать платежную учетную запись и проект.
- В Cloud Console щелкните раскрывающееся меню проекта и выберите проект, который вы хотите использовать для этой лаборатории кода.
- Включите API и SDK платформы Google Maps, необходимые для этой лаборатории кода, в Google Cloud Marketplace . Для этого выполните действия, описанные в этом видео или в этой документации .
- Создайте ключ API на странице учетных данных Cloud Console. Вы можете выполнить действия, описанные в этом видео или в этой документации . Для всех запросов к платформе Google Maps требуется ключ API.
3. Быстрый старт
Чтобы вы начали как можно быстрее, вот некоторый начальный код, который поможет вам следовать этой кодовой лаборатории. Мы приветствуем вас, чтобы перейти к решению, но если вы хотите выполнить все шаги, чтобы создать его самостоятельно, продолжайте читать.
- Клонируйте репозиторий, если у вас установлен
git
.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
Кроме того, вы можете нажать следующую кнопку, чтобы загрузить исходный код.
- Получив код, откройте проект, найденный в
starter
каталоге в Android Studio.
4. Добавьте Карты Google
В этом разделе вы добавите Карты Google, чтобы они загружались при запуске приложения.
Добавьте свой ключ API
Ключ API, который вы создали на предыдущем шаге, необходимо предоставить приложению, чтобы Maps SDK для Android мог связать ваш ключ с вашим приложением.
- Для этого откройте файл с именем
local.properties
в корневом каталоге вашего проекта (на том же уровне, гдеgradle.properties
иsettings.gradle
). - В этом файле определите новый ключ
GOOGLE_MAPS_API_KEY
, значением которого будет созданный вами ключ API.
местные.свойства
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
Обратите внимание, что local.properties
указан в файле .gitignore
в репозитории Git. Это связано с тем, что ваш ключ API считается конфиденциальной информацией и, по возможности, не должен быть зарегистрирован в системе управления версиями.
- Затем, чтобы раскрыть API, чтобы его можно было использовать во всем приложении, включите плагин Secrets Gradle Plugin для Android в файл
build.gradle
вашего приложения, расположенный в каталогеapp/
и добавьте следующую строку в блокplugins
:
build.gradle на уровне приложения
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
Вам также потребуется изменить файл build.gradle
на уровне проекта, включив в него следующий путь к классам:
build.gradle на уровне проекта
buildscript {
dependencies {
// ...
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
}
}
Этот плагин сделает ключи, которые вы определили в файле local.properties
, доступными в качестве переменных сборки в файле манифеста Android и в качестве переменных в классе BuildConfig
, сгенерированном Gradle, во время сборки. Использование этого подключаемого модуля удаляет шаблонный код, который в противном случае был бы необходим для чтения свойств из local.properties
, чтобы к нему можно было получить доступ во всем приложении.
Добавить зависимость от Google Maps
- Теперь, когда ваш ключ API доступен внутри приложения, следующим шагом будет добавление зависимости Maps SDK для Android в файл
build.gradle
вашего приложения.
В начальном проекте, который поставляется с этой лабораторией кода, эта зависимость уже добавлена для вас.
build.gradle
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- Затем добавьте новый тег
meta-data
вAndroidManifest.xml
для передачи ключа API, созданного на предыдущем шаге. Для этого откройте этот файл в Android Studio и добавьте следующий тегmeta-data
в объектapplication
в файлеAndroidManifest.xml
, расположенном вapp/src/main
.
AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- Затем создайте новый файл макета с именем
activity_main.xml
в каталогеapp/src/main/res/layout/
и определите его следующим образом:
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>
Этот макет имеет единственный FrameLayout
, содержащий SupportMapFragment
. Этот фрагмент содержит базовый объект GoogleMaps
, который вы будете использовать на последующих этапах.
- Наконец, обновите класс
MainActivity
, расположенный вapp/src/main/java/com/google/codelabs/buildyourfirstmap
, добавив следующий код, чтобы переопределить методonCreate
, чтобы вы могли установить его содержимое с новым макетом, который вы только что создали.
Основная деятельность
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- Теперь запустите приложение. Теперь вы должны увидеть загрузку карты на экране вашего устройства.
5. Облачный стиль карты (необязательно)
Вы можете настроить стиль своей карты, используя облачные стили карты .
Создать идентификатор карты
Если вы еще не создали идентификатор карты со связанным с ним стилем карты, см. руководство по идентификаторам карт , чтобы выполнить следующие шаги:
- Создайте идентификатор карты.
- Свяжите идентификатор карты со стилем карты.
Добавление идентификатора карты в ваше приложение
Чтобы использовать созданный вами идентификатор карты, измените файл activity_main.xml
и передайте свой идентификатор карты в map:mapId
объекта 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" />
После того, как вы закончите это, запустите приложение, чтобы увидеть свою карту в выбранном вами стиле!
6. Добавьте маркеры
В этой задаче вы добавляете на карту маркеры, представляющие достопримечательности, которые вы хотите выделить на карте. Сначала вы получаете список мест, предоставленных вам в начальном проекте, а затем добавляете эти места на карту. В данном примере это велосипедные магазины.
Получить ссылку на GoogleMap
Во-первых, вам нужно получить ссылку на объект GoogleMap
, чтобы вы могли использовать его методы. Для этого добавьте следующий код в метод MainActivity.onCreate()
сразу после вызова setContentView()
:
MainActivity.onCreate()
val mapFragment = supportFragmentManager.findFragmentById(
R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
addMarkers(googleMap)
}
Реализация сначала находит SupportMapFragment
, который вы добавили на предыдущем шаге, используя метод SupportFragmentManager
findFragmentById()
для объекта SupportFragmentManager. После получения ссылки вызывается вызов getMapAsync()
с последующей передачей лямбда-выражения. В эту лямбду передается объект GoogleMap
. Внутри этой лямбды вызывается вызов метода addMarkers()
, который вскоре будет определен.
Предоставленный класс: PlacesReader
В стартовом проекте вам был предоставлен класс PlacesReader
. Этот класс считывает список из 49 мест, хранящихся в файле JSON с именем places.json
и возвращает их в виде List<Place>
. Сами места представляют собой список велосипедных магазинов в Сан-Франциско, Калифорния, США.
Если вам интересно узнать о реализации этого класса, вы можете получить к нему доступ на GitHub или открыть класс PlacesReader
в 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()
}
}
Загрузить места
Чтобы загрузить список велосипедных магазинов, добавьте в MainActivity
свойство с именем places
и определите его следующим образом:
MainActivity.places
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
Этот код вызывает метод read()
для PlacesReader
, который возвращает List<Place>
. У Place
есть свойство name
, название места и latLng
— координаты, где это место расположено.
Место
data class Place(
val name: String,
val latLng: LatLng,
val address: LatLng,
val rating: Float
)
Добавьте маркеры на карту
Теперь, когда список мест загружен в память, следующим шагом будет представление этих мест на карте.
- Создайте метод в
MainActivity
с именемaddMarkers()
и определите его следующим образом:
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)
)
}
}
Этот метод выполняет итерацию по списку places
, после чего вызывает метод addMarker()
для предоставленного объекта GoogleMap
. Маркер создается путем создания экземпляра объекта MarkerOptions
, который позволяет настраивать сам маркер. В этом случае предоставляется заголовок и позиция маркера, который представляет название магазина велосипедов и его координаты соответственно.
- Запустите приложение и отправляйтесь в Сан-Франциско, чтобы увидеть маркеры, которые вы только что добавили!
7. Настройте маркеры
Для только что добавленных маркеров есть несколько вариантов настройки, чтобы они выделялись и доносили до пользователей полезную информацию. В этом задании вы изучите некоторые из них, настроив изображение каждого маркера, а также информационное окно, отображаемое при касании маркера.
Добавление информационного окна
По умолчанию информационное окно при нажатии на маркер отображает его заголовок и фрагмент (если он установлен). Вы настраиваете его так, чтобы он мог отображать дополнительную информацию, такую как адрес места и рейтинг.
Создайте файл marker_info_contents.xml
Сначала создайте новый файл макета с именем marker_info_contents.xml
.
- Для этого щелкните правой кнопкой мыши папку
app/src/main/res/layout
в представлении проекта в Android Studio и выберите New > Layout Resource File .
- В диалоговом окне введите
marker_info_contents
в поле « Имя файла» иLinearLayout
в поле «Root element
», затем нажмите « ОК ».
Этот файл макета позже расширяется для представления содержимого в информационном окне.
- Скопируйте содержимое следующего фрагмента кода, который добавляет три элемента
TextViews
в вертикальную группу представленийLinearLayout
, и перезапишите код по умолчанию в файле.
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>
Создайте реализацию InfoWindowAdapter
После создания файла макета пользовательского информационного окна следующим шагом будет реализация интерфейса GoogleMap.InfoWindowAdapter . Этот интерфейс содержит два метода: getInfoWindow()
и getInfoContents()
. Оба метода возвращают необязательный объект View
, причем первый используется для настройки самого окна, а второй — для настройки его содержимого. В вашем случае вы реализуете оба и настраиваете возврат getInfoContents()
при возврате null в getInfoWindow()
, что указывает на то, что следует использовать окно по умолчанию.
- Создайте новый файл Kotlin с именем
MarkerInfoWindowAdapter
в том же пакете, что иMainActivity
, щелкнув правой кнопкой мыши папкуapp/src/main/java/com/google/codelabs/buildyourfirstmap
в представлении проекта в Android Studio, затем выберите « Создать » > «Файл/класс Kotlin» . .
- В диалоговом окне введите
MarkerInfoWindowAdapter
и оставьте выделенным File .
- После создания файла скопируйте содержимое следующего фрагмента кода в новый файл.
Маркеринфовиндоадаптер
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
}
}
В содержимом метода getInfoContents()
предоставленный маркер в методе приводится к типу Place
, и если приведение невозможно, метод возвращает значение null (вы еще не установили свойство тега для Marker
, но вы сделайте это на следующем шаге).
Затем макет marker_info_contents.xml
, после чего текст, содержащий TextViews
, устанавливается в тег Place
.
Обновить основную активность
Чтобы склеить все компоненты, которые вы создали до сих пор, вам нужно добавить две строки в ваш класс MainActivity
.
Во-первых, чтобы передать пользовательский InfoWindowAdapter
, MarkerInfoWindowAdapter
, внутри вызова метода getMapAsync
, вызовите метод setInfoWindowAdapter()
для объекта GoogleMap
и создайте новый экземпляр MarkerInfoWindowAdapter
.
- Для этого добавьте следующий код после вызова метода
addMarkers()
внутри лямбда-getMapAsync()
.
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
Наконец, вам нужно установить каждое место в качестве свойства тега для каждого маркера, добавленного на карту.
- Для этого измените
places.forEach{}
в функцииaddMarkers()
следующим образом:
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
}
Добавить собственное изображение маркера
Настройка изображения маркера — один из забавных способов сообщить тип места, которое маркер представляет на вашей карте. На этом шаге вы отображаете велосипеды вместо красных маркеров по умолчанию, чтобы обозначить каждый магазин на карте. Стартовый проект включает значок велосипеда ic_directions_bike_black_24dp.xml
в app/src/res/drawable
, который вы используете.
Установить собственное растровое изображение на маркер
Имея в своем распоряжении значок векторного рисования велосипеда, следующим шагом будет установка этого рисунка в качестве значка каждого маркера на карте. MarkerOptions
есть icon
метода, который принимает BitmapDescriptor
, который вы используете для выполнения этой задачи.
Во-первых, вам нужно преобразовать вектор, который вы только что добавили, в BitmapDescriptor
. Файл с именем BitMapHelper
, включенный в начальный проект, содержит вспомогательную функцию с именем 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)
}
}
Этот метод принимает Context
, идентификатор ресурса для рисования, а также целое число цвета и создает его представление BitmapDescriptor
.
С помощью вспомогательного метода объявите новое свойство с именем bicycleIcon
и дайте ему следующее определение: 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)
}
Это свойство использует предопределенный цвет colorPrimary
в вашем приложении и использует его, чтобы подкрасить значок велосипеда и вернуть его как BitmapDescriptor
.
- Используя это свойство, вызовите метод
icon
MarkerOptions
вaddMarkers()
, чтобы завершить настройку значка. При этом свойство маркера должно выглядеть так:
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- Запустите приложение, чтобы увидеть обновленные маркеры!
8. Кластерные маркеры
В зависимости от того, насколько далеко вы увеличили карту, вы могли заметить, что добавленные вами маркеры перекрываются. С перекрывающимися маркерами очень сложно взаимодействовать, и они создают много шума, что влияет на удобство использования вашего приложения.
Чтобы улучшить взаимодействие с пользователем, всякий раз, когда у вас есть большой набор данных, который тесно сгруппирован, рекомендуется реализовать кластеризацию маркеров. При кластеризации, когда вы увеличиваете и уменьшаете масштаб карты, маркеры, которые находятся в непосредственной близости, группируются вместе следующим образом:
Чтобы реализовать это, вам понадобится помощь Maps SDK for Android Utility Library .
Maps SDK для библиотеки служебных программ Android
Библиотека служебных программ Maps SDK для Android была создана как способ расширить функциональные возможности Maps SDK для Android. Он предлагает расширенные функции, такие как кластеризация маркеров, тепловые карты, поддержка KML и GeoJson, кодирование и декодирование полилиний, а также несколько вспомогательных функций для сферической геометрии.
Обновите свой build.gradle
Поскольку служебная библиотека упакована отдельно от Maps SDK для Android, вам необходимо добавить дополнительную зависимость в файл build.gradle
.
- Идите вперед и обновите раздел
dependencies
вашего файлаapp/build.gradle
.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- После добавления этой строки вам необходимо выполнить синхронизацию проекта, чтобы получить новые зависимости.
Реализовать кластеризацию
Чтобы реализовать кластеризацию в своем приложении, выполните следующие три шага:
-
ClusterItem
интерфейс ClusterItem. - Подкласс класса
DefaultClusterRenderer
. - Создайте
ClusterManager
и добавьте элементы.
Реализовать интерфейс ClusterItem
Все объекты, представляющие кластеризуемый маркер на карте, должны реализовать интерфейс ClusterItem
. В вашем случае это означает, что модель Place
должна соответствовать ClusterItem
. Откройте файл Place.kt
и внесите в него следующие изменения:
Место
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 определяет эти три метода:
-
getPosition()
, который представляетLatLng
места. -
getTitle()
, который представляет название места -
getSnippet()
, который представляет адрес места.
Подкласс класса DefaultClusterRenderer
Класс, отвечающий за реализацию кластеризации, ClusterManager
, внутренне использует класс ClusterRenderer
для управления созданием кластеров при панорамировании и масштабировании карты. По умолчанию он поставляется с рендерером по умолчанию, DefaultClusterRenderer
, который реализует ClusterRenderer
. Для простых случаев этого должно быть достаточно. Однако в вашем случае, поскольку маркеры необходимо настраивать, вам необходимо расширить этот класс и добавить туда настройки.
Идем дальше и создаем файл Kotlin PlaceRenderer.kt
в пакете com.google.codelabs.buildyourfirstmap.place
и определяем его следующим образом:
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
}
}
Этот класс переопределяет эти две функции:
-
onBeforeClusterItemRendered()
, который вызывается перед визуализацией кластера на карте. Здесь вы можете предоставить настройки черезMarkerOptions
— в этом случае он устанавливает заголовок, положение и значок маркера. -
onClusterItemRenderer()
, который вызывается сразу после рендеринга маркера на карте. Здесь вы можете получить доступ к созданному объектуMarker
— в данном случае он устанавливает свойство тега маркера.
Создайте ClusterManager и добавьте элементы
Наконец, чтобы кластеризация заработала, вам нужно изменить MainActivity
, чтобы создать экземпляр ClusterManager
и предоставить ему необходимые зависимости. ClusterManager
обрабатывает добавление маркеров (объектов ClusterItem
) внутри себя, поэтому вместо добавления маркеров непосредственно на карту эта ответственность делегируется ClusterManager
. Кроме того, ClusterManager
также вызывает setInfoWindowAdapter()
внутренне, поэтому установка пользовательского информационного окна должна выполняться в ClusterManger
MarkerManager.Collection
.
- Для начала измените содержимое лямбды в
getMapAsync()
вMainActivity.onCreate()
. Идите вперед и закомментируйте вызовaddMarkers()
иsetInfoWindowAdapter()
и вместо этого вызовите методaddClusteredMarkers()
, который вы определите далее.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- Затем в
MainActivity
определите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()
}
}
Этот метод создает экземпляр ClusterManager
, передает ему пользовательский рендерер PlacesRenderer
, добавляет все места и вызывает метод cluster()
. Кроме того, поскольку ClusterManager
использует метод setInfoWindowAdapter()
для объекта карты, настройку пользовательского информационного окна необходимо выполнить для объекта ClusterManager.markerCollection
. Наконец, поскольку вы хотите, чтобы кластеризация менялась по мере того, как пользователь перемещает и масштабирует карту, для OnCameraIdleListener
предоставляется googleMap
, так что, когда камера простаивает, clusterManager.onCameraIdle()
.
- Идите вперед и запустите приложение, чтобы увидеть новые сгруппированные магазины!
9. Рисовать на карте
Хотя вы уже изучили один способ рисования на карте (путем добавления маркеров), Maps SDK для Android поддерживает множество других способов рисования для отображения полезной информации на карте.
Например, если вы хотите представить маршруты и области на карте, вы можете использовать ломаные линии и многоугольники для их отображения на карте. Или, если вы хотите зафиксировать изображение на поверхности земли, вы можете использовать наложения на землю .
В этом задании вы научитесь рисовать фигуры, в частности круг, вокруг маркера при каждом нажатии на него.
Добавить прослушиватель кликов
Как правило, вы добавляете прослушиватель кликов к маркеру, передавая прослушиватель кликов непосредственно объекту GoogleMap
с помощью setOnMarkerClickListener()
. Однако, поскольку вы используете кластеризацию, прослушиватель щелчков должен быть предоставлен ClusterManager
вместо этого.
- В
addClusteredMarkers()
вMainActivity
добавьте следующую строку сразу после вызоваcluster()
.
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
Этот метод добавляет прослушиватель и вызывает метод addCircle()
, который вы определяете далее. Наконец, из этого метода возвращается false
, чтобы указать, что этот метод не использовал это событие.
- Далее вам нужно определить
circle
свойств и методaddCircle()
в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))
)
}
Свойство circle
установлено таким образом, что всякий раз, когда нажимается новый маркер, предыдущий круг удаляется и добавляется новый. Обратите внимание, что API для добавления круга очень похож на добавление маркера.
- Идите вперед и запустите приложение, чтобы увидеть изменения.
10. Управление камерой
В качестве последней задачи вы смотрите на некоторые элементы управления камерой , чтобы вы могли сфокусировать вид на определенной области.
Камера и вид
Если вы заметили, что при запуске приложения камера показывает африканский континент, и вам приходится кропотливо панорамировать и приближать Сан-Франциско, чтобы найти добавленные маркеры. Хотя это может быть интересным способом исследовать мир, это бесполезно, если вы хотите сразу отобразить маркеры.
Чтобы помочь с этим, вы можете установить положение камеры программно, чтобы представление было отцентровано там, где вы хотите.
- Идем дальше и добавляем следующий код в
getMapAsync()
, чтобы настроить вид камеры так, чтобы он инициализировался в Сан-Франциско при запуске приложения.
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))
}
}
Во-первых, setOnMapLoadedCallback()
, чтобы обновление камеры выполнялось только после загрузки карты. Этот шаг необходим, поскольку свойства карты, такие как размеры, должны быть вычислены перед вызовом обновления камеры.
В лямбде создается новый объект LatLngBounds
, определяющий прямоугольную область на карте. Это постепенно строится путем включения в него всех значений LatLng
места, чтобы гарантировать, что все места находятся внутри границ. После создания этого объекта вызывается метод moveCamera()
в GoogleMap
, и ему передается CameraUpdate через CameraUpdate
CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
.
- Запустите приложение и обратите внимание, что камера теперь инициализирована в Сан-Франциско.
Прослушивание смены камеры
Помимо изменения положения камеры, вы также можете прослушивать обновления камеры, когда пользователь перемещается по карте. Это может быть полезно, если вы хотите изменить пользовательский интерфейс при перемещении камеры.
Ради интереса вы модифицируете код, чтобы сделать маркеры полупрозрачными при перемещении камеры.
- В
addClusteredMarkers()
добавьте следующие строки в конец метода:
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 }
}
Это добавляет OnCameraMoveStartedListener
, так что всякий раз, когда камера начинает двигаться, альфа-значения всех маркеров (как кластеров, так и маркеров) изменяются на 0.3f
, чтобы маркеры казались полупрозрачными.
- Наконец, чтобы изменить полупрозрачные маркеры обратно на непрозрачные, когда камера останавливается, измените содержимое
setOnCameraIdleListener
вaddClusteredMarkers()
следующим образом:
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()
}
- Идите вперед и запустите приложение, чтобы увидеть результаты!
11. Карты КТХ
Для приложений Kotlin, использующих один или несколько Android SDK платформы Google Maps, доступны расширения Kotlin или библиотеки KTX, которые позволяют вам использовать преимущества языковых функций Kotlin, таких как сопрограммы, свойства/функции расширений и многое другое. Каждый SDK Google Maps имеет соответствующую библиотеку KTX, как показано ниже:
В этой задаче вы будете использовать библиотеки Maps KTX и Maps Utils KTX в своем приложении и реорганизовать реализации предыдущих задач, чтобы вы могли использовать языковые функции, характерные для Kotlin, в своем приложении.
- Включите зависимости KTX в файл build.gradle на уровне приложения.
Поскольку приложение использует как Maps SDK для Android, так и Maps SDK для Android Utility Library, вам потребуется включить соответствующие библиотеки KTX для этих библиотек. В этой задаче вы также будете использовать функцию, найденную в библиотеке AndroidX Lifecycle KTX, поэтому включите эту зависимость в файл build.gradle
на уровне приложения.
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'
}
- Используйте функции расширения GoogleMap.addMarker() и GoogleMap.addCircle().
Библиотека Maps KTX предоставляет альтернативу API в стиле DSL для GoogleMap.addMarker(MarkerOptions)
и GoogleMap.addCircle(CircleOptions)
использовались на предыдущих шагах. Чтобы использовать вышеупомянутые API, необходимо создать класс, содержащий параметры для маркера или круга, тогда как с альтернативами KTX вы можете установить параметры маркера или круга в предоставленной вами лямбде.
Чтобы использовать эти API, обновите MainActivity.addMarkers(GoogleMap)
и 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))
}
}
Переписывание вышеприведенных методов таким образом намного более лаконично для чтения, что стало возможным с помощью литерала функции Kotlin с ресивером .
- Используйте функции приостановки расширения SupportMapFragment.awaitMap() и GoogleMap.awaitMapLoad().
Библиотека Maps KTX также предоставляет расширения функций приостановки для использования в сопрограммах. В частности, существуют альтернативные функции приостановки для SupportMapFragment.getMapAsync(OnMapReadyCallback)
и GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
. Использование этих альтернативных API устраняет необходимость передачи обратных вызовов и вместо этого позволяет получать ответ этих методов последовательным и синхронным способом.
Поскольку эти методы приостанавливают функции, их использование должно происходить внутри сопрограммы. Библиотека Lifecycle Runtime KTX предлагает расширение для предоставления областей сопрограмм с учетом жизненного цикла, чтобы сопрограммы запускались и останавливались в соответствующем событии жизненного цикла.
Объединив эти концепции, обновите метод 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)
}
}
Область действия сопрограммы lifecycleScope.launchWhenCreated
будет выполнять блок, когда действие находится как минимум в созданном состоянии. Также обратите внимание, что вызовы для получения объекта GoogleMap
и ожидания завершения загрузки карты были заменены на SupportMapFragment.awaitMap()
и GoogleMap.awaitMapLoad()
соответственно. Рефакторинг кода с использованием этих функций приостановки позволяет последовательно писать эквивалентный код на основе обратного вызова.
- Идите вперед и перестройте приложение с вашими рефакторинговыми изменениями!
12. Поздравления
Поздравляем! Вы рассмотрели много контента и, надеюсь, лучше понимаете основные функции, предлагаемые в Maps SDK для Android.
Учить больше
- Places SDK для Android — изучите богатый набор данных о местах, чтобы найти компании вокруг вас.
- android-maps-ktx — библиотека с открытым исходным кодом, позволяющая интегрироваться с Maps SDK для Android и Maps SDK для Android Utility Library удобным для Kotlin способом.
- android-place-ktx — библиотека с открытым исходным кодом, позволяющая интегрироваться с Places SDK для Android удобным для Kotlin способом.
- android-samples — пример кода на GitHub, демонстрирующий все функции, описанные в этой лаборатории кода, и многое другое.
- Другие кодовые лаборатории Kotlin для создания приложений Android с платформой Google Maps