Yandex MapKit — это кроссплатформенная библиотека, позволяющая использовать картографические данные и технологии Яндекса в мобильных приложениях. Список доступных возможностей действительно впечатляет, но разработчику, впервые столкнувшемуся с необходимостью работать с Яндекс-картами, многое может показаться непонятным и неочевидным в использовании.
Поэтому, чтобы научиться применять полезные особенности MapKit’a, мы с вами напишем небольшое приложение, в которое внедрим и настроим данную библиотеку: откроем определённую область на карте; выставим метку в нужном месте; установим на неё желаемые растровые и векторные изображения; поиграемся с зумом; обработаем нажатие на пин; а также будем при клике визуально выделять объекты на карте и получать от них интересующую нас информацию.
Содержание
1) Введение: внедрение и настройка Yandex MapKit в проекте
2) Открываем определённую область на карте
3) Устанавливаем метку на карте
4) Использование векторных изображений
5) Работа с зумом: меняем иконку маркера при отдалении и приближении камеры
6) Обработка события нажатия на метку
7) Выделение объекта на карте
8) Получаем информацию об объекте при тапе на него
Заключение
Список используемой литературы
Код программы
1) Введение: внедрение и настройка Yandex MapKit в проекте
Чтобы создать и запустить приложение с Яндекс-картами, первым делом необходимо:
Получить ключ;
Установить библиотеку MapKit;
Настроить библиотеку;
Собрать и запустить приложение.
Эти пункты весьма понятно и доступно описаны в документации. Дабы не копировать информацию, читателям предлагается самим ознакомиться, повторить данные шаги и затем запустить приложение с Яндекс-картой. Отметим, что в нашем приложении мы будем использовать полную full-версию библиотеки (на момент написания статьи - версия 4.3.1-full), а писать код на языке Kotlin, используя View Binding.
Отметим несколько важных моментов:
Какими знаниями вы должны обладать: Kotlin базовый уровень; умение собрать проект, запустить приложение на эмуляторе или телефоне, загрузить необходимые библиотеки; View Binding.
Ознакомьтесь с условиями использования MapKit. Так, например, нельзя скрывать логотип Яндекса на карте за другими объектами. Также, в вашем приложении в разделе «о программе» должна быть ссылка на условия использования Яндекс-карт.
API-ключ должен быть задан единожды перед инициализацией MapKitFactory. Хорошим тоном будет задать ключ при запуске приложения в методе
Application.onCreate()
, а инициализировать уже в других необходимых активити и фрагментах. Если же при каких-то условиях будет повторно вызван MapKitFactory.setApiKey("Ваш API-ключ"), вы получите краш приложения и ошибку в логах: "java.lang.AssertionError: You need to set the API key before using MapKit!". Примером появления подобной ошибки может быть следующий сценарий: переход с одного экрана во фрагмент с Яндекс-картами, где мы задаём API-ключ (карта при этом откроется и будет адекватно работать) -> возвращаемся на предыдущий экран -> вновь открываем фрагмент с картами -> происходит краш приложения.Допущения в данном проекте: в случае, если по каким-то причинам (как, например, в нашем приложении), логика работы карт и API-ключ находятся в одном активити/фрагменте, раздувать макет необходимо только после того, как установлен API-ключ. Иначе, проявится ошибка, указанная в предыдущем пункте. Также необходимо учесть момент пересоздания активити/фрагмента, например, для случая изменения ориентации экрана, вследствие чего вновь будет вызван метод MapKitFactory.setApiKey("Ваш API-ключ"). Воспользуемся проверкой: установили ли мы ранее API-ключ для Яндекс-карт. Для этого сохраним данную информацию:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean("haveApiKey", true)}
А при создании активности, в методе onCreate будем проверять, был ли уже установлен API-ключ ранее при помощи функции setApiKey:
private fun setApiKey(savedInstanceState: Bundle?) {
val haveApiKey = savedInstanceState?.getBoolean("haveApiKey") ?: false // При первом запуске приложения всегда false
if (!haveApiKey) {
MapKitFactory.setApiKey(MAPKIT_API_KEY)}} // API-ключ должен быть задан единожды перед инициализацией MapKitFactory
Что же, надеюсь, у вас получилось собрать проект со своей первой Яндекс-картой и перед вами на экране отобразились материки, моря да океаны:
А код выглядит примерно следующим образом:
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setApiKey(savedInstanceState) // Проверяем: был ли уже ранее установлен API-ключ в приложении. Если нет - устанавливаем его.
MapKitFactory.initialize(this) // Инициализация библиотеки для загрузки необходимых нативных библиотек.
binding = ActivityMainBinding.inflate(layoutInflater) // Раздуваем макет только после того, как установили API-ключ
setContentView(binding.root) // Размещаем пользовательский интерфейс в экране активности
}
private fun setApiKey(savedInstanceState: Bundle?) {
val haveApiKey = savedInstanceState?.getBoolean("haveApiKey") ?: false // При первом запуске приложения всегда false
if (!haveApiKey) {
MapKitFactory.setApiKey(MAPKIT_API_KEY) // API-ключ должен быть задан единожды перед инициализацией MapKitFactory
}
}
// Если Activity уничтожается (например, при нехватке памяти или при повороте экрана) - сохраняем информацию, что API-ключ уже был получен ранее
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean("haveApiKey", true)
}
// Отображаем карты перед моментом, когда активити с картой станет видимой пользователю:
override fun onStart() {
super.onStart()
MapKitFactory.getInstance().onStart()
binding.mapview.onStart()
}
// Останавливаем обработку карты, когда активити с картой становится невидимым для пользователя:
override fun onStop() {
binding.mapview.onStop()
MapKitFactory.getInstance().onStop()
super.onStop()
}
companion object {
const val MAPKIT_API_KEY = "Ваш API-ключ"
}
}
Соответствующая разметка .xml:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<com.yandex.mapkit.mapview.MapView
android:id="@+id/mapview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Теперь, когда все готово, перейдём к ещё более интересным и полезным моментам – интерактивам с картой. Пример полного кода будет приведён в конце статьи, а также его можно найти в GitHub.
2) Открываем определённую область на карте
Первым делом сделаем так, чтобы при открытии карты нам сразу показывалась определённая область. Здесь и далее, при необходимости импорта, выбираем библиотеку Яндекса. Добавим новые переменные: startLocation
– точку, содержащую координаты (широту и долготу), в которую должна переместиться камера и zoomValue – величину приближения к данной точке:
private val startLocation = Point(59.9402, 30.315)
private var zoomValue: Float = 16.5f
Создадим функцию moveToStartLocation()
, в которой у нашей mapview для карты вызовем метод, перемещающий камеру к необходимой позиции:
private fun moveToStartLocation() {
binding.mapview.map.move(
CameraPosition(startLocation, zoomValue, 0.0f, 0.0f)
)
}
Конструктор класса CameraPosition принимает, соответственно: точку с координатами, величину необходимого приближения, азимут и наклон (наклон камеры в градусах, добавляет визуально ощущение 3d). Максимальное возможное значение зума – 21.0f, минимальное – 0.0f. Также можем вызвать перегруженный метод move, чтобы перемещение в нужную область происходило красиво со стартовой анимацией:
binding.mapview.map.move(
CameraPosition(startLocation, zoomValue, 0.0f, 0.0f),
Animation(SMOOTH, 5f),
null)
Здесь, помимо CameraPosition, дополнительно необходимо указать параметры анимации (которая зависит от типа animationType и длительности duration) и функцию CameraCallback
, которая принимает логический аргумент, обозначающий завершение действия камеры. Если движение камеры по каким-то причинам прерывается, то в качестве аргумента передаётся «false»; если движение камеры завершилось успешно – «true». CameraCallback
имеет необязательный тип, т.е. может быть не инициализирован – укажем здесь null.
Не забываем вызвать moveToStartLocation()
в методе onCreate – и можем проверять работу перемещения камеры на стартовую локацию:
3) Устанавливаем метку на карте
Отметим нашу стартовую точку пином. Для начала загрузим в проект иконку с расширением png – скачайте файл ic_pin_png
и перетащите его в папку drawable проекта.
Заранее создадим две переменные, которые проинициализируем и о которых более подробно расскажем далее:
private lateinit var mapObjectCollection: MapObjectCollection
private lateinit var placemarkMapObject: PlacemarkMapObject
Реализуем функцию setMarkerInStartLocation()
, отвечающую за установку метки на карте. В ней первым делом создаём ссылку на нашу картинку - marker. Затем инициализируем коллекцию различных возможных объектов на карте mapObjectCollection, которая может содержать любой набор элементов MapObject. MapObject – это пользовательский объект, отображаемый на карте, например, метка с иконкой или геометрическая фигура. Создадим такой геопозиционированный объект PlacemarkMapObject
, являющийся наследником MapObject – метку с иконкой - который будет располагаться по координате, используемой ранее.
Для этого к mapObjectCollection
применим метод addPlacemark
, в котором укажем необходимую точку с координатами и, с помощью класса ImageProvider
, ссылку на иконку. Таким образом мы создадим новую метку с нашим изображением и добавим её в текущую коллекцию. Если указать только точку с координатами, тогда получится пин со значком и стилем по умолчанию (будет выглядеть, как обычная точка на карте).
Дополнительно, к placemarkMapObject
мы можем применить различные свойства, например, установить прозрачность метки и текст сверху в придачу при помощи команд setOpacity и setText соответственно. В итоге, произведя все вышеперечисленные действия, мы имеем следующую функцию:
setMarkerInStartLocation()
private fun setMarkerInStartLocation() {
val marker = R.drawable.ic_pin_black_png // Добавляем ссылку на картинку
mapObjectCollection = binding.mapview.map.mapObjects // Инициализируем коллекцию различных объектов на карте
placemarkMapObject = mapObjectCollection.addPlacemark(startLocation, ImageProvider.fromResource(this, marker)) // Добавляем метку со значком
placemarkMapObject.opacity = 0.5f // Устанавливаем прозрачность метке
placemarkMapObject.setText("Обязательно к посещению!") // Устанавливаем текст сверху метки
}
Вызываем setMarkerFirstOpen()
в onCreate, запускаем приложение и наблюдаем метку над Эрмитажем.
4) Использование векторных изображений
Обычно мы используем не png файлы, а векторные изображения. Тут имеется одна неприятная особенность: векторные изображения в качестве маркеров в MapKit не поддерживаются – они просто не будут отображаться. Но данный момент можно обойти. ImageProvider умеет работать с bitmap. Таким образом, нам стоит просто перевести векторное изображение в bitmap и использовать по назначению. Для этого создадим соответствующую функцию:
createBitmapFromVector(...)
private fun createBitmapFromVector(art: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(this, art) ?: return null
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
) ?: return null
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
Используем её: загрузите приложенные векторные изображения ic_pin_black_svg, ic_pin_blue_svg и ic_pin_red_svg. Затем добавьте их в свой проект: правой кнопкой по папке drawable -> Vector Asset -> Local file -> установите размер 64dp X 64dp -> Finish.
После этого в функции setMarkerInStartLocation()
исправим marker и placemarkMapObject
на:
val marker = createBitmapFromVector(R.drawable.ic_pin_black_svg)
placemarkMapObject = mapObjectCollection.addPlacemark(startLocation, ImageProvider.fromBitmap(marker))
Таким образом мы можем использовать в качестве иконок для меток и векторные и растровые изображения.
5) Работа с зумом: меняем иконку маркера при отдалении и приближении камеры
Теперь обработаем моменты увеличения и уменьшения масштаба карты пользователем: пусть при пересечении некоторой границы величины зума маркер становится красным при отдалении камеры и синим при приближении. Данную величину границы зума сразу вынесем в константу в companion object:
const val ZOOM_BOUNDARY = 16.4f
Наследуемся от интерфейса CameraListener
, который позволяет следить за обновлениями положения камеры:
class MainActivity : AppCompatActivity(), CameraListener {
и переопределяем метод onCameraPositionChanged
, который срабатывает при изменении положения камеры:
override fun onCameraPositionChanged(
map: Map,
cameraPosition: CameraPosition,
cameraUpdateReason: CameraUpdateReason,
finished: Boolean
) {}
Здесь параметрами являются:
map
- новая область карты (будьте внимательны при импорте – это не коллекция Map);cameraPosition
- текущее положение камеры;cameraUpdateReason
- причина обновления камеры. Это enum-класс, включающий в себя две константы: APPLICATION – т.е. причиной обновления камеры является вызов приложением метода move; GESTURES – причиной являются действия пользователя, такие как масштабирование, поворот и прочее;finished
– завершилось ли движение камеры окончательно. Будет «true», если камера закончила движение, «false» - в противном случае.
Обсудим логику работы изменения иконки метки при регулировании масштаба отображения карты. После того как пользователь успешно приблизил или отдалил карту, т.е. finished==true, проверим величину нового зума. Если величина зума cameraPosition.zoom будет меньше фиксированного значения ZOOM_BOUNDARY - мы поменяем изображение метки на красную иконку. Иначе, будет установлен пин синего цвета. Так же, чтобы каждый раз не производить излишнюю замену иконки при изменении масштаба пользователем, когда порог ZOOM_BOUNDARY не был преодолён, добавим дополнительную проверку: стала ли новая величина зума больше или меньше фиксированного значения? Для этого будем перезаписывать значение zoomValue после каждого изменения масштаба карты пользователем. В итоге имеем:
onCameraPositionChanged(...)
override fun onCameraPositionChanged(
map: Map,
cameraPosition: CameraPosition,
cameraUpdateReason: CameraUpdateReason,
finished: Boolean
) {
if (finished) { // Если камера закончила движение
when {
cameraPosition.zoom >= ZOOM_BOUNDARY && zoomValue <= ZOOM_BOUNDARY -> {
placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_blue_svg)))
}
cameraPosition.zoom <= ZOOM_BOUNDARY && zoomValue >= ZOOM_BOUNDARY -> {
placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_red_svg)))
}
}
zoomValue = cameraPosition.zoom // После изменения позиции камеры сохраняем величину зума
}
}
И последний штрих: необходимо инициализировать метод слушателя камеры, чтобы логика, прописанная в onCameraPositionChanged
, срабатывала. В onCreate добавляем следующую команду и после проверяем в приложении изменение иконки метки при регулировании масштаба:
binding.mapview.map.addCameraListener(this)
6) Обработка события нажатия на метку
Перейдем к обработке события нажатия на пин – выведем всплывающее окно Toast с надписью.
Для этого нам понадобится интерфейс MapObjectTapListener, как раз отвечающий за тапы по различным объектам на карте. Создадим экземпляр класса MapObjectTapListener и переопределим onMapObjectTap, который возвращает «true», если событие было обработано, иначе – «false». При этом, мы можем использовать информацию о mapObject’е и точке с координатами, по которой произошёл тап. Мы же просто воспользуемся Toast’ом с фиксированным текстом:
private val mapObjectTapListener = object : MapObjectTapListener {
override fun onMapObjectTap(mapObject: MapObject, point: Point): Boolean{
Toast.makeText(applicationContext, "Эрмитаж — музей изобразительных искусств", Toast.LENGTH_SHORT).show()
return true
}
}
Интерфейс MapObjectTapListener может быть присоединён к любому MapObject’у. Подключим этот слушатель в функции setMarkerFirstOpen()
к нашей метке placemarkMapObject
:
placemarkMapObject.addTapListener(mapObjectTapListener)
Готово! Теперь, при тапе на метку, будет появляться всплывающее окно с необходимой информацией.
Отдельно отметим, почему мы создаем отдельную переменную mapObjectTapListener
, а не сразу пишем в функции setMarkerFirstOpen
подобным образом:
placemarkMapObject.addTapListener(object : MapObjectTapListener {
override fun onMapObjectTap(mapObject: MapObject, point: Point): Boolean{
Toast.makeText(applicationContext, "Эрмитаж — музей изобразительных искусств", Toast.LENGTH_SHORT).show()
return true}
})
Дело в том, что MapKit хранит слабые ссылки на передаваемые ему Listener-объекты, поэтому их необходимо сохранять на стороне приложения. Иначе, первое время клики будут работать адекватно, а затем перестанут реагировать на тапы. Это связано с тем, что сборщик мусора, который не учитывает связь ссылки и объекта в куче при выявлении объектов, подлежащих удалению, в какой-то момент удалит наш слушатель. После этого при тапе на пин в логах возникнет неприятное сообщение: «yandex.maps.runtime: Java object is already finalized. Nothing to do» и клики перестанут обрабатываться должным образом. Поэтому, необходимо использовать строгую ссылку на объект MapObjectTapListener, что мы и делаем выше.
Бонусные разделы
Весьма полезной и интересной темой в Mapkit является получения информации об объекте, используя поиск и метаданные. Поскольку наш туториал рассчитан на новичков, сейчас мы не будем подробно разбираться в том, как именно устроен поиск и как работать с метаданными в mapkit. Но, в качестве дополнительной полезной информации, без особой конкретики и не раскрывая темы в полном объёме, приведём примеры кода с использованием этих моментов в пунктах 7*) и 8*). Более подробно прочитать о данных возможностях Mapkit можно в статье «Поиск в MapKit: Tips & Tricks».
7) Выделение объекта на карте
Продолжим знакомство со слушателями объектов на карте. Теперь будем работать с GeoObject – объектом в слоях карты, примером которого может выступать здание или памятник. Слушателем будет выступать экземпляр класса GeoObjectTapListener, в котором требуется переопределить onObjectTap. Метод onObjectTap позволяет извлечь краткую информацию затронутого геообъекта и возвращает булевское значение. Так, если мы вернём «false» – тогда событие клика распространится на карту, и его сможет перехватить другой слушатель. Если «true» – «дальше» событие никуда не пойдёт. Пример: если пользователь клацает на объект, а в проекте имеется дополнительный слушатель, обрабатывающий действия по всей карте (как, например, в пункте 8), то при значении «true» для onObjectTap данное событие более перехвачено не будет.
Давайте подсветим какое-либо здание при тапе на него, иначе говоря, отобразим для пользователя «выделение» объекта. Для этого нам понадобятся идентификаторы объекта и слоя. Получим геообъект из geoObjectTapEvent методом getGeoObject. Информация про геообъект хранится в метаданных. Метаданные бывают разных видов, более подробно можно ознакомиться в «Поиск в MapKit: Tips & Tricks». Доступ к информации можно получить с помощью метода getMetadataContainer(), для которого с помощью метода getItem указываем ключ для этого контейнера - тип необходимых метаданных. У нас данный ключ – GeoObjectSelectionMetadata.
После того как мы получили метаданные, для карты mapview используем метод selectGeoObject, в который передаем необходимые идентификаторы объекта и слоя:
tapListener
private val tapListener = object : GeoObjectTapListener {
override fun onObjectTap(geoObjectTapEvent: GeoObjectTapEvent): Boolean {
val selectionMetadata: GeoObjectSelectionMetadata = geoObjectTapEvent
.geoObject
.metadataContainer
.getItem(GeoObjectSelectionMetadata::class.java)
binding.mapview.map.selectGeoObject(selectionMetadata.id, selectionMetadata.layerId)
return false
}
}
Подключаем данный слушатель:
binding.mapview.map.addTapListener(tapListener) // Добавляем карте слушатель тапов по объектам
Теперь, при клике на здание, оно выделится следующим образом:
8) Получаем информацию об объекте при тапе на него
Осуществим следующую идею: при тапе в любую область карты будет появляться всплывающее окно с информацией об улице в данном месте. Если по каким-то причинам улицы нет, например, если тапнули по реке или не пришел ответ от сервера – будем выводить соответствующее сообщение.
В переопределённой функции onSearchResponse, последовательно используя различные вызовы, мы сможем докопаться до интересующей нас сущности – будь то улица, дом, маршрут, страна и прочее. А данную информацию можем использовать уже как угодно, например, поместить в Toast:
searchListener
private val searchListener = object : Session.SearchListener {
override fun onSearchResponse(response: Response) {
val street = response.collection.children.firstOrNull()?.obj
?.metadataContainer
?.getItem(ToponymObjectMetadata::class.java)
?.address
?.components
?.firstOrNull { it.kinds.contains(Address.Component.Kind.STREET)}
?.name ?: "Информация об улице не найдена"
Toast.makeText(applicationContext, street, Toast.LENGTH_SHORT).show()
}
override fun onSearchError(p0: Error) {
}
}
Предварительно создадим две новые переменные: поисковую сессию searchSession и интерфейс для начала поиска searchManager, которые проинициализируем далее:
lateinit var searchManager: SearchManager
lateinit var searchSession: Session
Теперь searchListener надо поместить в другой слушатель – inputListener, который обрабатывает тапы по всей карте. В нем переопределяются два метода – короткое и длинное нажатие. onMapTap - вызывается при быстром касании, если оно не было обработано геообъектами или объектами карты. Как уже говорилось ранее в пункте 7: если касание было обработано onObjectTap, то onMapTap может не сработать. Чтобы метод адекватно обработал тап – в onObjectTap необходимо вернуть «false». В самом методе onMapTap инициализируем поисковую сессию searchSession:
private val inputListener = object : InputListener {
override fun onMapTap(map: Map, point: Point) {
searchSession = searchManager.submit(point, 20, SearchOptions(), searchListener)
}
override fun onMapLongTap(map: Map, point: Point) {}
}
Метод обратного поиска submit
требует на вход точку с координатами, величину зума (в окрестности которой будет происходить поиск), настройки поиска и слушатель.
После, в методе onCreate инициализируем searchManager и добавляем к нашей карте слушатель тапов по карте с извлечением информации:
searchManager = SearchFactory.getInstance().createSearchManager(SearchManagerType.ONLINE)
binding.mapview.map.addInputListener(inputListener) // Добавляем слушатель тапов по карте с извлечением информации
В итоге, после проведенных манипуляций, при клике на какое-либо место в городе на экране будет появляться название соответствующей улицы:
Заключение
Yandex MapKit предоставляет гигантское количество возможностей, с помощью которых можно реализовать самые различные интересные и полезные идеи. Сегодня мы познакомились с основными моментами использования Яндекс-карт: были показаны часто используемые методы, даны различные рекомендации и хитрости для проблемных мест, ответы на вероятно возникающие вопросы при первом знакомстве с данной библиотекой.
Данная статья не является исчерпывающей, наоборот - с помощью Yandex MapKit’а можно решить множество разнообразных задач: построить маршрут для пешей прогулки иль поездки на общественном транспорте; отобразить на карте ближайший к пользователю банкомат; найти нужную организацию; узнать о пробках на дорогах в реальном времени; предоставить местоположение пользователя; использовать панорамы; получить какую-либо информацию об объекте на карте и многое, многое другое.
Дополнительно ознакомиться с примерами реализации части функционала можно здесь.
Искренне надеюсь, что полученные сегодня знания ускорили ваш порог вхождения в Яндекс MapKit, дали понимание, каким образом можно взаимодействовать и работать с Яндекс-картами в плане разработки.
Дерзайте! И помните: «Дорогу осилит идущий».
Список используемой литературы:
Код программы
Код программы
class MainActivity : AppCompatActivity(), CameraListener {
private lateinit var binding: ActivityMainBinding
private lateinit var mapObjectCollection: MapObjectCollection // Коллекция различных объектов на карте
private lateinit var placemarkMapObject: PlacemarkMapObject // Геопозиционированный объект (метка со значком) на карте
private val startLocation = Point(59.9402, 30.315) // Координаты Эрмитажа
private var zoomValue: Float = 16.5f // Величина зума
lateinit var searchManager: SearchManager
lateinit var searchSession: Session
private val mapObjectTapListener = object : MapObjectTapListener {
override fun onMapObjectTap(mapObject: MapObject, point: Point): Boolean {
Toast.makeText(applicationContext, "Эрмитаж — музей изобразительных искусств", Toast.LENGTH_SHORT).show()
return true
}
}
private val geoObjectTapListener = object : GeoObjectTapListener {
override fun onObjectTap(geoObjectTapEvent: GeoObjectTapEvent): Boolean {
val selectionMetadata: GeoObjectSelectionMetadata = geoObjectTapEvent
.geoObject
.metadataContainer
.getItem(GeoObjectSelectionMetadata::class.java)
binding.mapview.map.selectGeoObject(selectionMetadata.id, selectionMetadata.layerId)
return false
}
}
private val searchListener = object : Session.SearchListener {
override fun onSearchResponse(response: Response) {
val street = response.collection.children.firstOrNull()?.obj
?.metadataContainer
?.getItem(ToponymObjectMetadata::class.java)
?.address
?.components
?.firstOrNull { it.kinds.contains(Address.Component.Kind.STREET) }
?.name ?: "Информация об улице не найдена"
Toast.makeText(applicationContext, street, Toast.LENGTH_SHORT).show()
}
override fun onSearchError(p0: Error) {
}
}
private val inputListener = object : InputListener {
override fun onMapTap(map: Map, point: Point) {
searchSession = searchManager.submit(point, 20, SearchOptions(), searchListener)
}
override fun onMapLongTap(map: Map, point: Point) {}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setApiKey(savedInstanceState) // Проверяем: был ли уже ранее установлен API-ключ в приложении. Если нет - устанавливаем его.
MapKitFactory.initialize(this) // Инициализация библиотеки для загрузки необходимых нативных библиотек.
binding = ActivityMainBinding.inflate(layoutInflater) // Раздуваем макет только после того, как установили API-ключ
setContentView(binding.root) // Размещаем пользовательский интерфейс в экране активности
moveToStartLocation() // Перемещаем камеру в определенную область на карте
setMarkerInStartLocation() // Устанавливаем маркер на карте
binding.mapview.map.addCameraListener(this) // Добавляем карте слушатель камеры для слежки за изменением величины зума
binding.mapview.map.addTapListener(geoObjectTapListener) // Добавляем слушатель тапов по объектам
searchManager = SearchFactory.getInstance().createSearchManager(SearchManagerType.ONLINE)
binding.mapview.map.addInputListener(inputListener) // Добавляем слушатель тапов по карте с извлечением информации
}
override fun onCameraPositionChanged(
map: Map,
cameraPosition: CameraPosition,
cameraUpdateReason: CameraUpdateReason,
finished: Boolean
) {
if (finished) { // Если камера закончила движение
when {
cameraPosition.zoom >= ZOOM_BOUNDARY && zoomValue <= ZOOM_BOUNDARY -> {
placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_blue_svg)))
}
cameraPosition.zoom <= ZOOM_BOUNDARY && zoomValue >= ZOOM_BOUNDARY -> {
placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_red_svg)))
}
}
zoomValue = cameraPosition.zoom // После изменения позиции камеры сохраняем величину зума
}
}
private fun setMarkerInStartLocation() {
val marker = createBitmapFromVector(R.drawable.ic_pin_black_svg)
mapObjectCollection = binding.mapview.map.mapObjects // Инициализируем коллекцию различных объектов на карте
placemarkMapObject =
mapObjectCollection.addPlacemark(startLocation, ImageProvider.fromBitmap(marker)) // Добавляем метку со значком
placemarkMapObject.opacity = 0.5f // Устанавливаем прозрачность метке
placemarkMapObject.setText("Обязательно к посещению!") // Устанавливаем текст сверху метки
placemarkMapObject.addTapListener(mapObjectTapListener)
}
private fun createBitmapFromVector(art: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(this, art) ?: return null
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
) ?: return null
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun moveToStartLocation() {
binding.mapview.map.move(
CameraPosition(startLocation, zoomValue, 0.0f, 0.0f), // Позиция камеры
Animation(Type.SMOOTH, 2f), // Красивая анимация при переходе на стартовую точку
null
)
}
private fun setApiKey(savedInstanceState: Bundle?) {
val haveApiKey = savedInstanceState?.getBoolean("haveApiKey") ?: false // При первом запуске приложения всегда false
if (!haveApiKey) {
MapKitFactory.setApiKey(MAPKIT_API_KEY) // API-ключ должен быть задан единожды перед инициализацией MapKitFactory
}
}
// Если Activity уничтожается (например, при нехватке памяти или при повороте экрана) - сохраняем информацию, что API-ключ уже был получен ранее
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean("haveApiKey", true)
}
// Отображаем карты перед тем моментом, когда активити с картой станет видимой пользователю:
override fun onStart() {
super.onStart()
MapKitFactory.getInstance().onStart()
binding.mapview.onStart()
}
// Останавливаем обработку карты, когда активити с картой становится невидимым для пользователя:
override fun onStop() {
binding.mapview.onStop()
MapKitFactory.getInstance().onStop()
super.onStop()
}
companion object {
const val MAPKIT_API_KEY = "Ваш API-ключ"
const val ZOOM_BOUNDARY = 16.4f
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<com.yandex.mapkit.mapview.MapView
android:id="@+id/mapview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Также код можно посмотреть в GitHub.