1. Прежде чем начать
В этой лабораторной работе вы научитесь интегрировать Maps SDK для Android с вашим приложением и использовать его основные функции, создав приложение, отображающее карту гор Колорадо, США, с использованием различных типов маркеров. Кроме того, вы научитесь рисовать на карте другие фигуры.
Вот как это будет выглядеть, когда вы закончите работу над кодлабом:
Предпосылки
- Базовые знания Kotlin, Jetpack Compose и разработки под Android
Что ты будешь делать?
- Включите и используйте библиотеку Maps Compose для Maps SDK для Android, чтобы добавить
GoogleMap
в приложение Android. - Добавляйте и настраивайте маркеры
- Нарисуйте полигоны на карте
- Программное управление точкой обзора камеры
Что вам понадобится
- Карт SDK для Android
- Аккаунт Google с включенной функцией выставления счетов
- Последняя стабильная версия Android Studio
- Устройство Android или эмулятор Android , на котором работает платформа API Google на базе Android 5.0 или выше (инструкции по установке см. в разделе Запуск приложений на эмуляторе Android ).
- Подключение к Интернету
2. Настройте
Для следующего шага включения вам необходимо включить Maps SDK для Android .
Настройте платформу Google Карт
Если у вас еще нет учетной записи Google Cloud Platform и проекта с включенным выставлением счетов, ознакомьтесь с руководством « Начало работы с Google Maps Platform», чтобы создать учетную запись для выставления счетов и проект.
- В Cloud Console щелкните раскрывающееся меню проектов и выберите проект, который вы хотите использовать для этой кодовой лаборатории.
- Включите API и SDK платформы Google Карт, необходимые для этой лабораторной работы, в Google Cloud Marketplace . Для этого следуйте инструкциям в этом видео или в этой документации .
- Сгенерируйте ключ API на странице «Учётные данные» в Cloud Console. Вы можете следовать инструкциям в этом видео или в этой документации . Для всех запросов к платформе Google Карт требуется ключ API.
3. Быстрый старт
Чтобы вы могли начать как можно быстрее, вот пример кода, который поможет вам разобраться с этой практической работой. Вы можете сразу перейти к решению, но если хотите выполнить все шаги по его созданию самостоятельно, продолжайте читать.
- Клонируйте репозиторий, если у вас установлен
git
.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Или вы можете нажать следующую кнопку, чтобы загрузить исходный код.
- Получив код, откройте проект, находящийся в
starter
каталоге Android Studio.
4. Добавьте свой API-ключ в проект.
В этом разделе описывается, как сохранить ключ API, чтобы приложение могло безопасно ссылаться на него. Не следует регистрировать ключ API в системе контроля версий, поэтому мы рекомендуем хранить его в файле secrets.properties
, который будет размещён в локальной копии корневого каталога вашего проекта. Подробнее о файле secrets.properties
см. в разделе Файлы свойств Gradle .
Чтобы упростить эту задачу, мы рекомендуем вам использовать плагин Secrets Gradle для Android .
Чтобы установить плагин Secrets Gradle для Android в вашем проекте Google Maps:
- В Android Studio откройте файл
build.gradle.kts
верхнего уровня и добавьте следующий код в элементdependencies
подbuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Откройте файл
build.gradle.kts
на уровне модуля и добавьте следующий код в элементplugins
.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- В файле
build.gradle.kts
на уровне модуля убедитесь, что дляtargetSdk
иcompileSdk
задано значение не менее 34. - Сохраните файл и синхронизируйте свой проект с Gradle .
- Откройте файл
secrets.properties
в каталоге верхнего уровня и добавьте следующий код. ЗаменитеYOUR_API_KEY
вашим ключом API. Сохраните свой ключ в этом файле, посколькуsecrets.properties
не подлежит проверке в системе контроля версий.MAPS_API_KEY=YOUR_API_KEY
- Сохраните файл.
- Создайте файл
local.defaults.properties
в каталоге верхнего уровня, в той же папке, что и файлsecrets.properties
, а затем добавьте следующий код. Этот файл предназначен для резервного хранения ключа API на случай, если файлMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
не будет найден, чтобы предотвратить сбои сборок. Это происходит, когда вы клонируете приложение из системы контроля версий, а файлsecrets.properties
ещё не создан локально для хранения ключа API. - Сохраните файл.
- В файле
AndroidManifest.xml
перейдите кcom.google.android.geo.API_KEY
и обновите атрибутandroid:value
. Если тег<meta-data>
отсутствует, создайте его как дочерний элемент тега<application>
.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- В Android Studio откройте файл
build.gradle.kts
на уровне модуля и отредактируйте свойствоsecrets
. Если свойствоsecrets
отсутствует, добавьте его. Отредактируйте свойства плагина, задав дляpropertiesFileName
значениеsecrets.properties
, дляdefaultPropertiesFileName
значениеlocal.defaults.properties
, и задайте остальные свойства.secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. Добавьте Google Карты
В этом разделе вы добавите карту Google, которая будет загружаться при запуске приложения.
Добавить карты Составить зависимости
Теперь, когда ваш ключ API доступен внутри приложения, следующим шагом будет добавление зависимости Maps SDK для Android в файл build.gradle.kts
вашего приложения. Для сборки с помощью Jetpack Compose используйте библиотеку Maps Compose , которая предоставляет элементы Maps SDK для Android в виде компонуемых функций и типов данных.
build.gradle.kts
В файле build.gradle.kts
уровня приложения замените некомпозитные зависимости Maps SDK for Android :
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
с их составными аналогами:
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
Добавить компонуемую карту Google
В MountainMap.kt
добавьте составной объект GoogleMap
внутрь составного объекта Box
, вложенного в составной объект MapMountain
.
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
Теперь соберите и запустите приложение. Узрите! Вы увидите карту, центр которой — пресловутый Нулевой остров , также известный как нулевая широта и нулевая долгота. Позже вы узнаете, как расположить карту в нужном месте и выбрать нужный масштаб, а пока празднуйте свою первую победу!
6. Облачная стилизация карт
Вы можете настроить стиль своей карты, используя облачный стиль карт .
Создать идентификатор карты
Если вы еще не создали идентификатор карты со связанным с ним стилем карты, см. руководство по идентификаторам карт , чтобы выполнить следующие шаги:
- Создайте идентификатор карты.
- Свяжите идентификатор карты со стилем карты.
Добавьте идентификатор карты в свое приложение
Чтобы использовать созданный вами идентификатор карты при создании экземпляра компонуемого объекта GoogleMap
, используйте идентификатор карты при создании объекта GoogleMapOptions
, который назначается параметру googleMapOptionsFactory
в конструкторе.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
После того как вы это сделаете, запустите приложение, чтобы увидеть карту в выбранном вами стиле!
7. Загрузите данные маркера.
Основная задача приложения — загрузить коллекцию гор из локального хранилища и отобразить их на GoogleMap
. На этом этапе вы познакомитесь с предоставленной инфраструктурой для загрузки данных о горах и их отображения в пользовательском интерфейсе.
Гора
Класс данных Mountain
содержит все данные о каждой горе.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
Обратите внимание, что позже горы будут разделены по высоте. Горы высотой не менее 14 000 футов (4250 м) называются четырнадцатитысячниками (Fourteeners) . В исходном коде есть функция расширения, которая выполнит эту проверку.
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
Класс MountainsScreenViewState
содержит все данные, необходимые для отображения представления. Он может находиться в состоянии Loading
или MountainList
в зависимости от того, завершена ли загрузка списка гор.
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
Предоставленные классы: MountainsRepository
и MountainsViewModel
В стартовом проекте вам предоставлен класс MountainsRepository
. Этот класс считывает список горных мест, хранящихся в GPS Exchange Format
(GPX-файле) top_peaks.gpx
. Вызов mountainsRepository.loadMountains()
возвращает StateFlow<List<Mountain>>
.
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
— это класс ViewModel
, который загружает коллекции гор и отображает эти коллекции, а также другие части состояния пользовательского интерфейса через mountainsScreenViewState
. mountainsScreenViewState
— это горячий StateFlow
, который пользовательский интерфейс может наблюдать как изменяемое состояние с помощью функции расширения collectAsState
.
Следуя принципам рациональной архитектуры, MountainsViewModel
хранит всё состояние приложения. Пользовательский интерфейс отправляет пользовательские взаимодействия модели представления с помощью метода onEvent
.
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
Если вам интересно узнать о реализации этих классов, вы можете получить к ним доступ на GitHub или открыть классы MountainsRepository
и MountainsViewModel
в Android Studio.
Используйте ViewModel
Модель представления используется в MainActivity
для получения viewState
. Вы будете использовать viewState
для отрисовки маркеров далее в этой лабораторной работе. Обратите внимание, что этот код уже включён в стартовый проект и показан здесь только для справки.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Расположите камеру
По умолчанию GoogleMap
центрируется на нулевой широте и нулевой долготе. Маркеры, которые вы будете отображать, находятся в штате Колорадо, США. viewState
предоставляемое моделью представления, представляет собой LatLngBounds , содержащий все маркеры.
В MountainMap.kt
создайте объект CameraPositionState
, инициализированный по центру ограничивающей рамки. Присвойте параметру cameraPositionState
объекта GoogleMap
значение только что созданной вами переменной cameraPositionState
.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Теперь запустите код и посмотрите, как центр карты окажется в Колорадо.
Масштабировать до границ маркера
Чтобы сфокусировать карту на маркерах, добавьте функцию zoomAll
в конец файла MountainMap.kt
. Обратите внимание, что для этой функции требуется CoroutineScope
, поскольку анимация перемещения камеры в новое местоположение — асинхронная операция, требующая времени.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
Затем добавьте код для вызова функции zoomAll
при каждом изменении границ коллекции маркеров или при нажатии пользователем кнопки изменения масштаба на панели TopApp. Обратите внимание, что кнопка изменения масштаба уже подключена для отправки событий в модель представления. Вам нужно только собирать эти события из модели представления и вызывать функцию zoomAll
в ответ.
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
Теперь при запуске приложения карта будет фокусироваться на области, куда будут помещены маркеры. Вы можете менять положение и масштаб, а нажатие кнопки изменения масштаба снова сфокусирует карту на области маркера. Это уже прогресс! Но на карте действительно должно быть что-то интересное. И именно этим мы займёмся на следующем шаге!
9. Основные маркеры
На этом этапе вы добавите на карту маркеры , обозначающие интересные места, которые вы хотите выделить на карте. Вы будете использовать список гор, предоставленный в стартовом проекте, и добавите эти места в качестве маркеров на карту.
Начните с добавления блока контента в GoogleMap
. Будет несколько типов маркеров, поэтому добавьте оператор when
для перехода к каждому типу, и вы по очереди реализуете каждый из них на следующих этапах.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Добавить маркеры
Добавьте аннотацию BasicMarkersMapContent
с помощью @GoogleMapComposable
. Обратите внимание, что использование функций @GoogleMapComposable
в блоке контента GoogleMap
ограничено. Объект mountains
содержит список объектов Mountain
. Вы добавите маркер для каждой горы из этого списка, используя местоположение, название и высоту из объекта Mountain
. Местоположение используется для задания параметра состояния объекта Marker
», который, в свою очередь, управляет положением маркера.
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
Запустите приложение и вы увидите маркеры, которые вы только что добавили!
Настроить маркеры
Для только что добавленных маркеров доступно несколько вариантов настройки, которые помогут им выделиться и донести полезную информацию до пользователей. В этом задании вы изучите некоторые из них, настроив изображение каждого маркера.
Начальный проект включает вспомогательную функцию vectorToBitmap
для создания BitmapDescriptor
из @DrawableResource
.
Стартовый код включает значок горы, baseline_filter_hdr_24.xml
, который вы будете использовать для настройки маркеров.
Функция vectorToBitmap
преобразует векторный объект в BitmapDescriptor
для использования с библиотекой карт. Цвета значков задаются с помощью экземпляра BitmapParameters
.
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
Используйте функцию vectorToBitmap
для создания двух настраиваемых BitmapDescriptor
: одного для четырнадцатитысячников и одного для обычных гор. Затем используйте параметр icon
объекта Marker
composable для установки значка. Также задайте параметр anchor
, чтобы изменить положение якоря относительно значка. Для круглых значков лучше использовать центр.
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
Запустите приложение и полюбуйтесь на настраиваемые маркеры. Чтобы увидеть полный набор гор, переключите переключатель Show all
. Маркеры на горах будут отличаться в зависимости от высоты горы (14 футов).
10. Продвинутые маркеры
AdvancedMarker
маркеры добавляют дополнительные функции к базовым Markers
. На этом этапе вы настроите поведение при столкновении и настроите стиль булавки.
Добавьте @GoogleMapComposable
к функции AdvancedMarkersMapContent
. Пройдитесь по mountains
, добавляя AdvancedMarker
для каждой горы.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
Обратите внимание на параметр collisionBehavior
. Если установить для него значение REQUIRED_AND_HIDES_OPTIONAL
, ваш маркер заменит любой маркер с более низким приоритетом. Это можно увидеть, увеличив масштаб базового маркера по сравнению с маркером с расширенным приоритетом. Базовый маркер, скорее всего, будет отображать ваш маркер и маркер в одном и том же месте на базовой карте. Расширенный маркер скроет маркер с более низким приоритетом.
Запустите приложение, чтобы увидеть расширенные маркеры. Выберите вкладку Advanced markers
в нижней строке навигации.
Индивидуальные AdvancedMarkers
Значки используют основную и дополнительную цветовые схемы для различения Четырнадцати гор и других гор. Используйте функцию vectorToBitmap
для создания двух BitmapDescriptor
: одного для Четырнадцати гор и одного для остальных. Используйте эти значки для создания собственного pinConfig
для каждого типа. Наконец, примените булавку к соответствующему AdvancedMarker
на основе функции is14er()
.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. Кластерные маркеры
На этом этапе вы будете использовать компонуемый элемент Clustering
для добавления группировки элементов на основе масштабирования.
Для компонуемого объекта Clustering
требуется коллекция объектов ClusterItem
. MountainClusterItem
реализует интерфейс ClusterItem
. Добавьте этот класс в файл ClusteringMarkersMapContent.kt
.
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
Теперь добавьте код для создания объектов MountainClusterItem
из списка гор. Обратите внимание, что этот код использует UnitsConverter
для преобразования единиц измерения в единицы, соответствующие пользователю в зависимости от его локали. Это настраивается в MainActivity
с помощью CompositionLocal
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
С этим кодом маркеры группируются в зависимости от уровня масштабирования. Красиво и аккуратно!
Настроить кластеры
Как и другие типы маркеров, кластерные маркеры можно настраивать. Параметр clusterItemContent
компонуемого объекта Clustering
задаёт пользовательский компонуемый блок для отрисовки некластеризованного элемента. Реализуйте функцию @Composable
для создания маркера. Функция SingleMountain
визуализирует компонуемый Icon
Material 3 с настраиваемой цветовой схемой фона.
В ClusteringMarkersMapContent.kt
создайте класс данных, определяющий цветовую схему для маркера:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
Кроме того, в ClusteringMarkersMapContent.kt
создайте компонуемую функцию для отрисовки значка для заданной цветовой схемы:
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
Теперь создайте цветовую схему для четырнадцатитысячников и ещё одну для остальных гор. В блоке clusterItemContent
выберите цветовую схему в зависимости от того, является ли данная гора четырнадцатитысячником.
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
Теперь запустите приложение, чтобы увидеть измененные версии отдельных элементов.
12. Нарисуйте на карте
Хотя вы уже изучили один способ рисования на карте (добавление маркеров), Maps SDK для Android поддерживает множество других способов рисования для отображения полезной информации на карте.
Например, если вы хотите обозначить маршруты и области на карте, вы можете использовать Polyline
и Polygon
. Или, если вы хотите прикрепить изображение к поверхности земли, можно использовать наложение GroundOverlay
.
В этом задании вы научитесь рисовать фигуры, в частности, контур штата Колорадо. Граница Колорадо проходит между 37° и 41° северной широты и 102° и 109°. Это значительно упрощает рисование контура.
Стартовый код включает класс DMS
для преобразования формата градусы-минуты-секунды в десятичные градусы.
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
С помощью класса DMS вы можете нарисовать границу Колорадо, определив координаты четырёх углов LatLng
и отобразив их в виде Polygon
. Добавьте следующий код в MountainMap.kt
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
Теперь вызовите ColoradoPolyon()
внутри блока контента GoogleMap
.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
Теперь приложение отображает контуры штата Колорадо, придавая им едва заметную заливку.
13. Добавьте слой KML и масштабную линейку.
В этом заключительном разделе вы в общих чертах обрисуете различные горные хребты и добавите к карте масштабную линейку.
Очертите горные хребты.
Ранее вы нарисовали контур вокруг Колорадо. Теперь вам предстоит добавить на карту более сложные фигуры. Начальный код включает файл Keyhole Markup Language (KML), который примерно описывает основные горные хребты. В библиотеке служебных программ Maps SDK для Android есть функция для добавления слоя KML на карту. В MountainMap.kt
добавьте вызов MapEffect
в блок контента GoogleMap
после блока when
. Функция MapEffect
вызывается с объектом GoogleMap
. Она может служить удобным связующим звеном между некомпозитными API и библиотеками, которым требуется объект GoogleMap
.
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
Добавить масштаб карты
В качестве заключительного задания вам нужно добавить масштаб на карту. ScaleBar
реализует масштаб, который можно добавить на карту. Обратите внимание, что ScaleBar
не является
@GoogleMapComposable
, поэтому его нельзя добавить в контент GoogleMap
. Вместо этого его можно добавить в Box
, содержащее карту.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Запустите приложение, чтобы увидеть полностью реализованную лабораторную работу.
14. Получите код решения
Чтобы загрузить код готовой лабораторной работы, вы можете использовать следующие команды:
- Клонируйте репозиторий, если у вас установлен
git
.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Или вы можете нажать следующую кнопку, чтобы загрузить исходный код.
- Получив код, откройте проект, находящийся в каталоге
solution
в Android Studio.
15. Поздравления
Поздравляем! Вы рассмотрели много материала и, надеемся, теперь лучше понимаете основные функции Maps SDK для Android.
Узнать больше
- Maps SDK для Android — создавайте динамические, интерактивные, настраиваемые карты, местоположения и геопространственные данные для своих приложений Android.
- Библиотека Maps Compose — набор компонуемых функций и типов данных с открытым исходным кодом, которые можно использовать с Jetpack Compose для создания своего приложения.
- android-maps-compose — пример кода на GitHub, демонстрирующий все функции, рассматриваемые в этой лабораторной работе, и многое другое.
- Больше практических занятий на Kotlin по созданию приложений Android с использованием платформы Google Maps