Добавьте карту в свое приложение для Android (Kotlin)

1. Прежде чем начать

В этой лабораторной работе вы узнаете, как интегрировать Maps SDK для Android с вашим приложением и использовать его основные функции, создавая приложение, отображающее карту велосипедных магазинов в Сан-Франциско, Калифорния, США.

f05e1ca27ff42bf6.png

Предпосылки

  • Базовые знания Kotlin и Android-разработки

Что ты будешь делать

  • Включите и используйте Maps SDK для Android, чтобы добавить Карты Google в приложение Android.
  • Добавляйте, настраивайте и группируйте маркеры.
  • Нарисуйте полилинии и многоугольники на карте.
  • Управляйте точкой обзора камеры программно.

Что вам понадобится

2. Настройте

Для следующего шага активации необходимо включить Maps SDK для Android .

Настройте платформу Google Карт

Если у вас еще нет учетной записи Google Cloud Platform и проекта с включенным выставлением счетов, ознакомьтесь с руководством по началу работы с платформой Google Maps , чтобы создать платежную учетную запись и проект.

  1. В Cloud Console щелкните раскрывающееся меню проекта и выберите проект, который вы хотите использовать для этой лаборатории кода.

  1. Включите API и SDK платформы Google Maps, необходимые для этой лаборатории кода, в Google Cloud Marketplace . Для этого выполните действия, описанные в этом видео или в этой документации .
  2. Создайте ключ API на странице учетных данных Cloud Console. Вы можете выполнить действия, описанные в этом видео или в этой документации . Для всех запросов к платформе Google Maps требуется ключ API.

3. Быстрый старт

Чтобы вы начали как можно быстрее, вот некоторый начальный код, который поможет вам следовать этой кодовой лаборатории. Мы приветствуем вас, чтобы перейти к решению, но если вы хотите выполнить все шаги, чтобы создать его самостоятельно, продолжайте читать.

  1. Клонируйте репозиторий, если у вас установлен git .
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

Кроме того, вы можете нажать следующую кнопку, чтобы загрузить исходный код.

  1. Получив код, откройте проект, найденный в starter каталоге в Android Studio.

4. Добавьте Карты Google

В этом разделе вы добавите Карты Google, чтобы они загружались при запуске приложения.

d1d068b5d4ae38b9.png

Добавьте свой ключ API

Ключ API, который вы создали на предыдущем шаге, необходимо предоставить приложению, чтобы Maps SDK для Android мог связать ваш ключ с вашим приложением.

  1. Для этого откройте файл с именем local.properties в корневом каталоге вашего проекта (на том же уровне, где gradle.properties и settings.gradle ).
  2. В этом файле определите новый ключ GOOGLE_MAPS_API_KEY , значением которого будет созданный вами ключ API.

местные.свойства

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Обратите внимание, что local.properties указан в файле .gitignore в репозитории Git. Это связано с тем, что ваш ключ API считается конфиденциальной информацией и, по возможности, не должен быть зарегистрирован в системе управления версиями.

  1. Затем, чтобы раскрыть 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

  1. Теперь, когда ваш ключ 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'
}
  1. Затем добавьте новый тег 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}" />
  1. Затем создайте новый файл макета с именем 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 , который вы будете использовать на последующих этапах.

  1. Наконец, обновите класс MainActivity , расположенный в app/src/main/java/com/google/codelabs/buildyourfirstmap , добавив следующий код, чтобы переопределить метод onCreate , чтобы вы могли установить его содержимое с новым макетом, который вы только что создали.

Основная деятельность

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Теперь запустите приложение. Теперь вы должны увидеть загрузку карты на экране вашего устройства.

5. Облачный стиль карты (необязательно)

Вы можете настроить стиль своей карты, используя облачные стили карты .

Создать идентификатор карты

Если вы еще не создали идентификатор карты со связанным с ним стилем карты, см. руководство по идентификаторам карт , чтобы выполнить следующие шаги:

  1. Создайте идентификатор карты.
  2. Свяжите идентификатор карты со стилем карты.

Добавление идентификатора карты в ваше приложение

Чтобы использовать созданный вами идентификатор карты, измените файл 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. Добавьте маркеры

В этой задаче вы добавляете на карту маркеры, представляющие достопримечательности, которые вы хотите выделить на карте. Сначала вы получаете список мест, предоставленных вам в начальном проекте, а затем добавляете эти места на карту. В данном примере это велосипедные магазины.

bc5576877369b554.png

Получить ссылку на 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
)

Добавьте маркеры на карту

Теперь, когда список мест загружен в память, следующим шагом будет представление этих мест на карте.

  1. Создайте метод в 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 , который позволяет настраивать сам маркер. В этом случае предоставляется заголовок и позиция маркера, который представляет название магазина велосипедов и его координаты соответственно.

  1. Запустите приложение и отправляйтесь в Сан-Франциско, чтобы увидеть маркеры, которые вы только что добавили!

7. Настройте маркеры

Для только что добавленных маркеров есть несколько вариантов настройки, чтобы они выделялись и доносили до пользователей полезную информацию. В этом задании вы изучите некоторые из них, настроив изображение каждого маркера, а также информационное окно, отображаемое при касании маркера.

a26f82802fe838e9.png

Добавление информационного окна

По умолчанию информационное окно при нажатии на маркер отображает его заголовок и фрагмент (если он установлен). Вы настраиваете его так, чтобы он мог отображать дополнительную информацию, такую ​​как адрес места и рейтинг.

Создайте файл marker_info_contents.xml

Сначала создайте новый файл макета с именем marker_info_contents.xml .

  1. Для этого щелкните правой кнопкой мыши папку app/src/main/res/layout в представлении проекта в Android Studio и выберите New > Layout Resource File .

8cac51fcbef9171b.png

  1. В диалоговом окне введите marker_info_contents в поле « Имя файла» и LinearLayout в поле « Root element », затем нажмите « ОК ».

8783af12baf07a80.png

Этот файл макета позже расширяется для представления содержимого в информационном окне.

  1. Скопируйте содержимое следующего фрагмента кода, который добавляет три элемента 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() , что указывает на то, что следует использовать окно по умолчанию.

  1. Создайте новый файл Kotlin с именем MarkerInfoWindowAdapter в том же пакете, что и MainActivity , щелкнув правой кнопкой мыши папку app/src/main/java/com/google/codelabs/buildyourfirstmap в представлении проекта в Android Studio, затем выберите « Создать » > «Файл/класс Kotlin» . .

3975ba36eba9f8e1.png

  1. В диалоговом окне введите MarkerInfoWindowAdapter и оставьте выделенным File .

992235af53d3897f.png

  1. После создания файла скопируйте содержимое следующего фрагмента кода в новый файл.

Маркеринфовиндоадаптер

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 .

  1. Для этого добавьте следующий код после вызова метода addMarkers() внутри лямбда- getMapAsync() .

MainActivity.onCreate()

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

Наконец, вам нужно установить каждое место в качестве свойства тега для каждого маркера, добавленного на карту.

  1. Для этого измените 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 , который вы используете.

6eb7358bb61b0a88.png

Установить собственное растровое изображение на маркер

Имея в своем распоряжении значок векторного рисования велосипеда, следующим шагом будет установка этого рисунка в качестве значка каждого маркера на карте. 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 .

  1. Используя это свойство, вызовите метод icon MarkerOptions в addMarkers() , чтобы завершить настройку значка. При этом свойство маркера должно выглядеть так:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Запустите приложение, чтобы увидеть обновленные маркеры!

8. Кластерные маркеры

В зависимости от того, насколько далеко вы увеличили карту, вы могли заметить, что добавленные вами маркеры перекрываются. С перекрывающимися маркерами очень сложно взаимодействовать, и они создают много шума, что влияет на удобство использования вашего приложения.

68591edc86d73724.png

Чтобы улучшить взаимодействие с пользователем, всякий раз, когда у вас есть большой набор данных, который тесно сгруппирован, рекомендуется реализовать кластеризацию маркеров. При кластеризации, когда вы увеличиваете и уменьшаете масштаб карты, маркеры, которые находятся в непосредственной близости, группируются вместе следующим образом:

f05e1ca27ff42bf6.png

Чтобы реализовать это, вам понадобится помощь Maps SDK for Android Utility Library .

Maps SDK для библиотеки служебных программ Android

Библиотека служебных программ Maps SDK для Android была создана как способ расширить функциональные возможности Maps SDK для Android. Он предлагает расширенные функции, такие как кластеризация маркеров, тепловые карты, поддержка KML и GeoJson, кодирование и декодирование полилиний, а также несколько вспомогательных функций для сферической геометрии.

Обновите свой build.gradle

Поскольку служебная библиотека упакована отдельно от Maps SDK для Android, вам необходимо добавить дополнительную зависимость в файл build.gradle .

  1. Идите вперед и обновите раздел dependencies вашего файла app/build.gradle .

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. После добавления этой строки вам необходимо выполнить синхронизацию проекта, чтобы получить новые зависимости.

b7b030ec82c007fd.png

Реализовать кластеризацию

Чтобы реализовать кластеризацию в своем приложении, выполните следующие три шага:

  1. ClusterItem интерфейс ClusterItem.
  2. Подкласс класса DefaultClusterRenderer .
  3. Создайте 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 .

  1. Для начала измените содержимое лямбды в getMapAsync() в MainActivity.onCreate() . Идите вперед и закомментируйте вызов addMarkers() и setInfoWindowAdapter() и вместо этого вызовите метод addClusteredMarkers() , который вы определите далее.

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Затем в 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() .

  1. Идите вперед и запустите приложение, чтобы увидеть новые сгруппированные магазины!

9. Рисовать на карте

Хотя вы уже изучили один способ рисования на карте (путем добавления маркеров), Maps SDK для Android поддерживает множество других способов рисования для отображения полезной информации на карте.

Например, если вы хотите представить маршруты и области на карте, вы можете использовать ломаные линии и многоугольники для их отображения на карте. Или, если вы хотите зафиксировать изображение на поверхности земли, вы можете использовать наложения на землю .

В этом задании вы научитесь рисовать фигуры, в частности круг, вокруг маркера при каждом нажатии на него.

f98ce13055430352.png

Добавить прослушиватель кликов

Как правило, вы добавляете прослушиватель кликов к маркеру, передавая прослушиватель кликов непосредственно объекту GoogleMap с помощью setOnMarkerClickListener() . Однако, поскольку вы используете кластеризацию, прослушиватель щелчков должен быть предоставлен ClusterManager вместо этого.

  1. В addClusteredMarkers() в MainActivity добавьте следующую строку сразу после вызова cluster() .

MainActivity.addClusteredMarkers()

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

Этот метод добавляет прослушиватель и вызывает метод addCircle() , который вы определяете далее. Наконец, из этого метода возвращается false , чтобы указать, что этот метод не использовал это событие.

  1. Далее вам нужно определить 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 для добавления круга очень похож на добавление маркера.

  1. Идите вперед и запустите приложение, чтобы увидеть изменения.

10. Управление камерой

В качестве последней задачи вы смотрите на некоторые элементы управления камерой , чтобы вы могли сфокусировать вид на определенной области.

Камера и вид

Если вы заметили, что при запуске приложения камера показывает африканский континент, и вам приходится кропотливо панорамировать и приближать Сан-Франциско, чтобы найти добавленные маркеры. Хотя это может быть интересным способом исследовать мир, это бесполезно, если вы хотите сразу отобразить маркеры.

Чтобы помочь с этим, вы можете установить положение камеры программно, чтобы представление было отцентровано там, где вы хотите.

  1. Идем дальше и добавляем следующий код в 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) .

  1. Запустите приложение и обратите внимание, что камера теперь инициализирована в Сан-Франциско.

Прослушивание смены камеры

Помимо изменения положения камеры, вы также можете прослушивать обновления камеры, когда пользователь перемещается по карте. Это может быть полезно, если вы хотите изменить пользовательский интерфейс при перемещении камеры.

Ради интереса вы модифицируете код, чтобы сделать маркеры полупрозрачными при перемещении камеры.

  1. В 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 , чтобы маркеры казались полупрозрачными.

  1. Наконец, чтобы изменить полупрозрачные маркеры обратно на непрозрачные, когда камера останавливается, измените содержимое 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()
}
  1. Идите вперед и запустите приложение, чтобы увидеть результаты!

11. Карты КТХ

Для приложений Kotlin, использующих один или несколько Android SDK платформы Google Maps, доступны расширения Kotlin или библиотеки KTX, которые позволяют вам использовать преимущества языковых функций Kotlin, таких как сопрограммы, свойства/функции расширений и многое другое. Каждый SDK Google Maps имеет соответствующую библиотеку KTX, как показано ниже:

Схема KTX платформы Google Maps

В этой задаче вы будете использовать библиотеки Maps KTX и Maps Utils KTX в своем приложении и реорганизовать реализации предыдущих задач, чтобы вы могли использовать языковые функции, характерные для Kotlin, в своем приложении.

  1. Включите зависимости 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'
}
  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 с ресивером .

  1. Используйте функции приостановки расширения 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() соответственно. Рефакторинг кода с использованием этих функций приостановки позволяет последовательно писать эквивалентный код на основе обратного вызова.

  1. Идите вперед и перестройте приложение с вашими рефакторинговыми изменениями!

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