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

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

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

Вот как это будет выглядеть, когда вы закончите работу над кодлабом:

Предпосылки

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

  • Включите и используйте библиотеку Maps Compose для Maps SDK для Android, чтобы добавить GoogleMap в приложение 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/googlemaps-samples/codelab-maps-platform-101-compose.git

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

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

4. Добавьте свой API-ключ в проект.

В этом разделе описывается, как сохранить ключ API, чтобы приложение могло безопасно ссылаться на него. Не следует регистрировать ключ API в системе контроля версий, поэтому мы рекомендуем хранить его в файле secrets.properties , который будет размещён в локальной копии корневого каталога вашего проекта. Подробнее о файле secrets.properties см. в разделе Файлы свойств Gradle .

Чтобы упростить эту задачу, мы рекомендуем вам использовать плагин Secrets Gradle для Android .

Чтобы установить плагин Secrets Gradle для Android в вашем проекте Google Maps:

  1. В Android Studio откройте файл build.gradle.kts верхнего уровня и добавьте следующий код в элемент dependencies под buildscript .
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Откройте файл build.gradle.kts на уровне модуля и добавьте следующий код в элемент plugins .
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. В файле build.gradle.kts на уровне модуля убедитесь, что для targetSdk и compileSdk задано значение не менее 34.
  4. Сохраните файл и синхронизируйте свой проект с Gradle .
  5. Откройте файл secrets.properties в каталоге верхнего уровня и добавьте следующий код. Замените YOUR_API_KEY вашим ключом API. Сохраните свой ключ в этом файле, поскольку secrets.properties не подлежит проверке в системе контроля версий.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. Сохраните файл.
  7. Создайте файл local.defaults.properties в каталоге верхнего уровня, в той же папке, что и файл secrets.properties , а затем добавьте следующий код.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    Этот файл предназначен для резервного хранения ключа API на случай, если файл secrets.properties не будет найден, чтобы предотвратить сбои сборок. Это происходит, когда вы клонируете приложение из системы контроля версий, а файл secrets.properties ещё не создан локально для хранения ключа API.
  8. Сохраните файл.
  9. В файле 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}" />
    
  10. В 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. Облачная стилизация карт

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

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

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

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

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

Чтобы использовать созданный вами идентификатор карты при создании экземпляра компонуемого объекта 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. Получите код решения

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

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

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

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

15. Поздравления

Поздравляем! Вы рассмотрели много материала и, надеемся, теперь лучше понимаете основные функции Maps SDK для Android.

Узнать больше

  • Maps SDK для Android — создавайте динамические, интерактивные, настраиваемые карты, местоположения и геопространственные данные для своих приложений Android.
  • Библиотека Maps Compose — набор компонуемых функций и типов данных с открытым исходным кодом, которые можно использовать с Jetpack Compose для создания своего приложения.
  • android-maps-compose — пример кода на GitHub, демонстрирующий все функции, рассматриваемые в этой лабораторной работе, и многое другое.
  • Больше практических занятий на Kotlin по созданию приложений Android с использованием платформы Google Maps