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.

Комментарии (3)


  1. Rusrst
    18.08.2023 11:13
    +3

    Разработчики Яндекс карт это прям отдельный вид людей - документация не просто на 0, она отрицательная. Сделать по ней что-то тот ещё квест. Но сами карты (в смысле движок) писали конечно умные люди...


  1. atoro
    18.08.2023 11:13
    +1

    Хорошая статья, прошедшей зимой сэкономила бы мне пару дней. От себя добавлю для сталкивающихся с мапкит в первый раз два момента, пермишены на FINE и COARSE в манифест яндекс прописывает за вас сам и сам же в момент инициализации библиотеки в активи/фрагменте под капотом пытается разово запросить текущую локацию.Было актуально по крайней мере на 4.2.2. А еще я в момент интеграции по докам так и не понял явно работает ли яндексовая локация на устройствах с HMS only, поэтому связывать с ней побоялся и сразу затянул системную. Переделывать уже не вижу смысла, но хотелось бы услышать от людей с соотвествующим опытом, не слишком ли я перестраховался:)


    1. Rusrst
      18.08.2023 11:13

      Работает, норм там все. Ей же сервисы не нужны.