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

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

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

f05e1ca27ff42bf6.png

Предпосылки

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

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

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

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

2. Настройте

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

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

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

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

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

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

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

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

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

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

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

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

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

MainActivity

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 , добавленный на предыдущем шаге, используя метод 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 и выберите «Создать» > «Файл ресурсов макета» .

8cac51fcbef9171b.png

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

8783af12baf07a80.png

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

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

маркер_информация_контента.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() , при этом getInfoWindow() возвращает значение null, что указывает на необходимость использования окна по умолчанию.

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

3975ba36eba9f8e1.png

  1. В диалоговом окне введите MarkerInfoWindowAdapter и оставьте выделенным пункт Файл .

992235af53d3897f.png

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

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

В содержимом метода getInfoContents() предоставленный в методе Marker приводится к типу Place , и если приведение невозможно, метод возвращает null (вы еще не установили свойство tag для Marker , но вы сделаете это на следующем шаге).

Затем заполняется макет marker_info_contents.xml , после чего в тег Place помещается текст, содержащий TextViews .

Обновление MainActivity

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

Библиотека служебных программ 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 .
  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 — в данном случае он устанавливает свойство tag маркера.

Создайте ClusterManager и добавьте элементы

Наконец, для работы кластеризации необходимо изменить MainActivity , чтобы создать экземпляр ClusterManager и предоставить ему необходимые зависимости. ClusterManager обрабатывает добавление маркеров (объектов ClusterItem ) самостоятельно, поэтому вместо добавления маркеров непосредственно на карту эта задача делегируется ClusterManager . Кроме того, ClusterManager также вызывает setInfoWindowAdapter() , поэтому настройку пользовательского информационного окна необходимо выполнить в объекте MarkerManager.Collection объекта ClusterManger .

  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 . Кроме того, поскольку требуется, чтобы кластеризация изменялась при панорамировании и масштабировании карты пользователем, в googleMap предоставляется слушатель OnCameraIdleListener , который при переходе камеры в режим ожидания вызывает 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 для добавления круга очень похож на 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 через 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. Карты KTX

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

Диаграмма KTX платформы Google Карт

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

  1. Включите зависимости KTX в файл build.gradle уровня приложения.

Поскольку приложение использует как Maps SDK для Android, так и библиотеку утилит Maps SDK для Android, вам потребуется включить соответствующие библиотеки 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 в удобном для Kotlin формате.
  • android-place-ktx — библиотека с открытым исходным кодом, позволяющая интегрироваться с Places SDK для Android способом, удобным для Kotlin.
  • android-samples — пример кода на GitHub, демонстрирующий все функции, рассматриваемые в этой лабораторной работе, и многое другое.
  • Больше практических занятий на Kotlin по созданию приложений Android с использованием платформы Google Maps