Карты — один из ключевых элементов многих мобильных приложений. И наш сервис не исключение. С помощью карт пользователь указывает, куда подать автомобиль, а водитель строит маршрут поездки. Кроме того, на карте в реальном времени отображается движение водителя к пассажиру и многое другое.
Когда я пришел в Android-команду inDriver в 2016 году, приложение работало с использованием Google Maps SDK. Тогда оно показывало на карте свободных водителей и то, где находится пассажир, после того, как он сделал заказ — то есть совсем немного.
С развитием сервиса карте стало уделяться больше внимания как с точки зрения дизайна, так и в плане разработки. Например, раньше первым, что видел пассажир, была голая форма заказа. Сейчас это форма заказа поверх интерактивной карты.
Но, однажды, из-за ограничений пользовательского соглашения было решено перейти на другой движок карт. После изучения доступных решений выбор пал на Open Source библиотеку osmdroid.
Эта библиотека довольно популярна, имеет обширное комьюнити, активно поддерживается и развивается. Задачу миграции поручили мне, и с тех пор я плотно занимаюсь картами нашего приложения в целом. В процессе миграции мы добавили наложение тайлов от 2GIS. Они хорошо детализированы и узнаваемы в странах СНГ, которые были на тот момент основным нашим рынком.
Буквально через год в связи с расширением компании на зарубежные рынки опять встал вопрос о картах. Ведь тайлов 2GIS для других стран нет, а стандартные от Open Streets Map выглядят скудно. В итоге было принято решение о возвращении Google Maps SDK в проект, но только для зарубежных стран.
Мы столкнулись с необходимостью отображать карты на различных движках в зависимости от страны/города. Кроме того, где-то нужно использовать тайлы, где-то — нет.
Почему нельзя просто использовать один движок и поверх него накладывать необходимые тайлы? Такой вариант невозможен из-за того, что политика Google не позволяет использовать тайлы Google Maps поверх другого движка.
Так же хотелось, чтобы разработчик, использующий карты в своей фиче, не задумывался, какая конкретная конфигурация используется в данный момент. Т.е. нужна полная инкапсуляция работы с картой и ее элементами.
Было решено сделать абстракцию над классами
Если верить описанию в
Как оказалось, у них есть небольшие отличия, на которые пришлось потратить время, чтобы все работало одинаково на обоих движках:
Для начала я создал абстрактный класс
Затем создал custom view, который будет использоваться разработчиками на их экранах. Вся его разметка состоит из одного
Этот
После заполнения вьюшки метод
В разметку добавляется
В
В подписке
Также создал абстракции над сущностями, связанными с картой:
Это требовалось для того, чтобы с ними тоже можно было работать со стороны фичи, например кастомизировать маркер или анимировать его.
Абстрактный метод принимает координаты центра карты и требуемый масштаб
Реализация для osmdroid
Реализация для Google Maps
Также есть расширения этого метода для анимированного движения карты и учета смещения центра карты.
Абстрактный метод принимает координату, на которую требуется поместить маркер и иконку самого маркера
Реализация для osmdroid
Реализация для Google Maps
Как и в предыдущем случае, есть расширенные методы для учета якоря маркера, для учета положения добавляемого маркера относительно других маркеров.
В последующем вынес всю работу с картой в отдельный gradle-модуль, подключаемый по необходимости в фичи.
Разработчикам фич не требуется детально разбираться в том, как устроены и работают карты в проекте. Достаточно добавить
Теперь есть возможность при любых проблемах на внешней стороне переключить пользователей на другой движок через админку. Весной нам это понадобилось и очень помогло.
23 апреля 2020 года приложения, использующие Google Maps SDK, начали массово вылетать. Судя по заведенной задаче https://issuetracker.google.com/issues/154855417, проблема коснулась и iOS приложений.
Поскольку в нашем приложении, а iOS команда сделала карты примерно так же, задолго до этого была реализована поддержка двух разных движков карт, достаточно было переключить пользователей на использование
Конечно, есть у этого подхода и минус.
В процессе использования такой системы мы периодически сталкиваемся с ситуациями, когда в одном движке либо что-то не работает, либо работает не так, как хотелось бы. Из последних могу привести пример с добавлением фичи отрисовки дуги между точками маршрута у пассажира. Казалось бы, задача решается добавлением нескольких
История
Когда я пришел в Android-команду inDriver в 2016 году, приложение работало с использованием Google Maps SDK. Тогда оно показывало на карте свободных водителей и то, где находится пассажир, после того, как он сделал заказ — то есть совсем немного.
С развитием сервиса карте стало уделяться больше внимания как с точки зрения дизайна, так и в плане разработки. Например, раньше первым, что видел пассажир, была голая форма заказа. Сейчас это форма заказа поверх интерактивной карты.
Но, однажды, из-за ограничений пользовательского соглашения было решено перейти на другой движок карт. После изучения доступных решений выбор пал на Open Source библиотеку osmdroid.
Эта библиотека довольно популярна, имеет обширное комьюнити, активно поддерживается и развивается. Задачу миграции поручили мне, и с тех пор я плотно занимаюсь картами нашего приложения в целом. В процессе миграции мы добавили наложение тайлов от 2GIS. Они хорошо детализированы и узнаваемы в странах СНГ, которые были на тот момент основным нашим рынком.
Буквально через год в связи с расширением компании на зарубежные рынки опять встал вопрос о картах. Ведь тайлов 2GIS для других стран нет, а стандартные от Open Streets Map выглядят скудно. В итоге было принято решение о возвращении Google Maps SDK в проект, но только для зарубежных стран.
Проблема
Мы столкнулись с необходимостью отображать карты на различных движках в зависимости от страны/города. Кроме того, где-то нужно использовать тайлы, где-то — нет.
Почему нельзя просто использовать один движок и поверх него накладывать необходимые тайлы? Такой вариант невозможен из-за того, что политика Google не позволяет использовать тайлы Google Maps поверх другого движка.
Так же хотелось, чтобы разработчик, использующий карты в своей фиче, не задумывался, какая конкретная конфигурация используется в данный момент. Т.е. нужна полная инкапсуляция работы с картой и ее элементами.
Решение
Было решено сделать абстракцию над классами
MapView
, предоставляемыми Google Maps SDK и osmdroid. Абстракция должна реализовываться классами-обертками, отвечающими за работу с конкретным движком карты. Необходимая реализация абстракции должна создаваться в зависимости от переданных от сервера параметров карты в текущем городе, с которой в дальнейшем и производятся все необходимые действия.Реализация
Если верить описанию в
README.md
, osmdroid является (практически) полной и свободной заменой для MapView
из состава Google Maps SDK. Прочитав это и представив, что все пройдет гладко (нет), я начал работу.Как оказалось, у них есть небольшие отличия, на которые пришлось потратить время, чтобы все работало одинаково на обоих движках:
- нет некоторых слушателей событий карты (например, завершение движения);
- нужна своя реализация точки текущего местоположения;
- обработка прикосновений реализована по-другому.
Для начала я создал абстрактный класс
MapView
, который инкапсулирует все взаимодействия с картой и ее события: двигать карту на координату, сделать зум, добавить маркер, добавить Polyline
и так далее. Его реализуют GoogleMapView
и OsmMapView
, которые уже содержат в себе непосредственный MapView
, предоставляемый движком карты.Затем создал custom view, который будет использоваться разработчиками на их экранах. Вся его разметка состоит из одного
ViewStub
:<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ViewStub android:id="@+id/map_view_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>
Этот
ViewStub
будет заполняться GoogleMapView
или OsmMapView
(в зависимости от переданных параметров). Оборачивание в merge
позволяет избавиться от лишней вложенности вьюшек.fun onCreate(type: String, bundle: Bundle?): MapView {
val stub = view.findViewById(R.id.map_view_stub) as ViewStub
stub.layoutResource = when (type) {
MAP_TYPE_GOOGLE -> R.layout.gmaps_layout
else -> R.layout.osm_layout
}
stub.inflate()
mapView = view.findViewById(R.id.map_impl) as MapView
mapView.onCreate(bundle)
return mapView
}
После заполнения вьюшки метод
onCreate
возвращает MapView
в виде абстракции, с которой и будут производиться все манипуляции. В общем виде для разработчика это выглядит так:В разметку добавляется
<MapWrapper
android:id="@+id/main_mapwrapper"
android:layout_width="match_parent"
android:layout_height="match_parent" />
В
activity/fragment
private lateinit var mapView: MapView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mapView = main_mapwrapper.onCreate(getMapType(), savedInstanceState)
disposables.add(
mapView.onMapReady()
.subscribe { onMapReady() }
)
}
В подписке
mapView.onMapReady
производится настройка карты и подписка на события, связанные с картой. Сделал через RxJava, т.к. инициализация Google Maps происходит асинхронно.private fun onMapReady() {
mapView.apply {
isZoomControlsEnabled = false
isMultiTouchControls = true
isTilesScaledToDpi = true
}
mapViewModel.onMapReady()
observe(mapViewModel.getMapLiveData(), ::handleMapLiveData)
}
Также создал абстракции над сущностями, связанными с картой:
- BaseMarker
- Polyline
- Projection
- InfoWindow
- Bounds
Это требовалось для того, чтобы с ними тоже можно было работать со стороны фичи, например кастомизировать маркер или анимировать его.
Пара примеров, как происходит взаимодействие с картой
Движение карты
Абстрактный метод принимает координаты центра карты и требуемый масштаб
abstract fun moveCamera(location: Location, zoom: Float)
Реализация для osmdroid
override fun moveCamera(location: Location, zoom: Float) {
with(mapView.controller) {
setZoom(zoom.toDouble())
setCenter(GeoPoint(location.latitude, location.longitude))
}
}
Реализация для Google Maps
override fun moveCamera(location: Location, zoom: Float) {
map.moveCamera(
CameraUpdateFactory.newLatLngZoom(
LatLng(
location.latitude,
location.longitude
), zoom
)
)
}
Также есть расширения этого метода для анимированного движения карты и учета смещения центра карты.
Добавление маркера
Абстрактный метод принимает координату, на которую требуется поместить маркер и иконку самого маркера
abstract fun addMarker(location: Location, icon: Drawable): BaseMarker
Реализация для osmdroid
override fun addMarker(location: Location, icon: Drawable): BaseMarker {
val osmMarker = Marker(mapView)
val marker = OsmMarker(osmMarker, mapView)
marker.location = location
marker.icon = icon
return marker
}
Реализация для Google Maps
override fun addMarker(location: Location, icon: Drawable): BaseMarker {
val googleMarker = map.addMarker(
MarkerOptions()
.position(LatLng(location.latitude, location.longitude))
)
val marker = GoogleMarker(googleMarker)
marker.icon = icon
return marker
}
Как и в предыдущем случае, есть расширенные методы для учета якоря маркера, для учета положения добавляемого маркера относительно других маркеров.
В последующем вынес всю работу с картой в отдельный gradle-модуль, подключаемый по необходимости в фичи.
Что получилось в итоге
Разработчикам фич не требуется детально разбираться в том, как устроены и работают карты в проекте. Достаточно добавить
custom view
в разметку своей вьюшки и использовать готовые методы через абстракцию. Если требуется реализовать новую фичу в картах, это будет делать команда, отвечающая за функционал карты.Теперь есть возможность при любых проблемах на внешней стороне переключить пользователей на другой движок через админку. Весной нам это понадобилось и очень помогло.
23 апреля 2020 года приложения, использующие Google Maps SDK, начали массово вылетать. Судя по заведенной задаче https://issuetracker.google.com/issues/154855417, проблема коснулась и iOS приложений.
Поскольку в нашем приложении, а iOS команда сделала карты примерно так же, задолго до этого была реализована поддержка двух разных движков карт, достаточно было переключить пользователей на использование
osmdroid
до тех пор, пока ошибка не будет исправлена.Конечно, есть у этого подхода и минус.
В процессе использования такой системы мы периодически сталкиваемся с ситуациями, когда в одном движке либо что-то не работает, либо работает не так, как хотелось бы. Из последних могу привести пример с добавлением фичи отрисовки дуги между точками маршрута у пассажира. Казалось бы, задача решается добавлением нескольких
Polyline
на карту, точки для которой предварительно рассчитываются. На деле оказалось, что Google Maps добавляет маркеры и Polyline
в один и тот же слой, при этом встроенные элементы карты (заведения, достопримечательности и т.д.) будут всегда выше этого слоя. Карта osmdroid
оказалась в этом плане более гибкой, позволяя создавать несколько слоев. Дальнейшим развитием фичи должна была стать анимация дуги, что тоже работало по-разному в обоих движках. Из-за таких ситуаций приходится тратить время на сравнение и изучение поведения в разных ситуациях.