Встраиваем карты от Huawei в Android приложение
В предыдущих статьях мы создавали аккаунт разработчика для использования Huawei Mobile Services и подготавливали проект к их использованию. Потом использовали аналитику от Huawei вместо аналога от Google. Также поступили и с определением геолокации. В этой же статье мы будем использовать карты от Huawei вместо карт от Google.
Вот полный список статей из цикла:
- Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. тык
- Встраиваем Huawei Analytics. тык
- Используем геолокацию от Huawei. тык
- Huawei maps. Используем вместо Google maps для AppGallery. ← вы тут
В чём сложность
К сожалению, с картами не получится так просто, как было с аналитикой и геолокацией. Что и неудивительно, т.к. это гораздо более сложная система сама по себе. И очень часто в приложениях карты и взаимодействие с ними кастомизируется. Например, отображают маркеры, кластеризуют их. Поэтому кода будет много, т.к. надо всё это заабстрагировать, имея в виду некоторые отличия в API карт разных реализаций.
Создаём абстракцию над картой
Надо в разметке использовать разные классы для отображения карты. com.google.android.libraries.maps.MapView
для гугло-карт и com.huawei.hms.maps.MapView
для Huawei. Сделаем так: создадим собственную абстрактную вьюху, унаследовавшись от FrameLayout
и в неё будет загружать конкретную реализацию MapView
в разных flavors
. Также создадим в нашей абстрактной вьюхе все нужные методы, которые мы должны вызывать на конкретных реализациях. И ещё метод для получения объекта самой карты. И методы для непосредственного внедрения реализации MapView от гугла и Huawei и прокидывания атрибутов для карт из разметки. Вот такой класс получится:
abstract class MapView : FrameLayout {
enum class MapType(val value: Int) {
NONE(0), NORMAL(1), SATELLITE(2), TERRAIN(3), HYBRID(4)
}
protected var mapType = MapType.NORMAL
protected var liteModeEnabled = false
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initView(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
initView(context, attrs)
}
private fun initView(context: Context, attrs: AttributeSet) {
initAttributes(context, attrs)
inflateMapViewImpl()
}
private fun initAttributes(context: Context, attrs: AttributeSet) {
val attributeInfo = context.obtainStyledAttributes(
attrs,
R.styleable.MapView
)
mapType = MapType.values()[attributeInfo.getInt(
R.styleable.MapView_someMapType,
MapType.NORMAL.value
)]
liteModeEnabled = attributeInfo.getBoolean(R.styleable.MapView_liteModeEnabled, false)
attributeInfo.recycle()
}
abstract fun inflateMapViewImpl()
abstract fun onCreate(mapViewBundle: Bundle?)
abstract fun onStart()
abstract fun onResume()
abstract fun onPause()
abstract fun onStop()
abstract fun onLowMemory()
abstract fun onDestroy()
abstract fun onSaveInstanceState(mapViewBundle: Bundle?)
abstract fun getMapAsync(function: (SomeMap) -> Unit)
}
Чтобы работали атрибуты в разметке нам, конечно, надо их определить. Добавляем в res/values/attrs.xml
вот это:
Это нам позволит прямо в разметке, используя нашу абстрактную карту передавать тип карты и нужен ли нам облегчённый режим для неё. Выглядеть в разметке это будет как-то так (реализация MapViewImpl
будет показана далее):
Как можно заметить в коде нашего абстрактного класса MapView
, там используется некий SomeMap
в методе getMapAsync
. Так что давайте сразу покажем какие ещё общие классы и интерфейсы нам понадобятся, прежде чем перейдём к использованию различных реализаций карт.
SomeMap
— основной класс для работы с картами. В его переопределениях мы будет прокидывать вызовы методов для показа маркеров, назначения слушателей событий и опций отображения и для перемещения камеры по карте:
abstract class SomeMap {
abstract fun setUiSettings(
isMapToolbarEnabled: Boolean? = null,
isCompassEnabled: Boolean? = null,
isRotateGesturesEnabled: Boolean? = null,
isMyLocationButtonEnabled: Boolean? = null,
isZoomControlsEnabled: Boolean? = null
)
abstract fun setPadding(left: Int, top: Int, right: Int, bottom: Int)
abstract fun animateCamera(someCameraUpdate: SomeCameraUpdate)
abstract fun moveCamera(someCameraUpdate: SomeCameraUpdate)
abstract fun setOnCameraIdleListener(function: () -> Unit)
abstract fun setOnCameraMoveStartedListener(onCameraMoveStartedListener: (Int) -> Unit)
abstract fun setOnCameraMoveListener(function: () -> Unit)
abstract fun setOnMarkerClickListener(function: (SomeMarker) -> Boolean)
abstract fun setOnMapClickListener(function: () -> Unit)
abstract fun addMarker(markerOptions: SomeMarkerOptions): SomeMarker
abstract fun - addMarkers(
context: Context,
markers: List
- ,
clusterItemClickListener: (Item) -> Boolean,
clusterClickListener: (SomeCluster
- ) -> Boolean,
generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)? = null
): (Item?) -> Unit
companion object {
const val REASON_GESTURE = 1
const val REASON_API_ANIMATION = 2
const val REASON_DEVELOPER_ANIMATION = 3
}
}
А вот и остальные классы/интерфейсы:
SomeCameraUpdate
— нужен для перемещения камеры на карте к какой-то точке или области.
class SomeCameraUpdate private constructor(
val location: Location? = null,
val zoom: Float? = null,
val bounds: SomeLatLngBounds? = null,
val width: Int? = null,
val height: Int? = null,
val padding: Int? = null
) {
constructor(
location: Location? = null,
zoom: Float? = null
) : this(location, zoom, null, null, null, null)
constructor(
bounds: SomeLatLngBounds? = null,
width: Int? = null,
height: Int? = null,
padding: Int? = null
) : this(null, null, bounds, width, height, padding)
}
SomeLatLngBounds
— класс для описания области на карте, куда можно переместить камеру.
abstract class SomeLatLngBounds(val southwest: Location? = null, val northeast: Location? = null) {
abstract fun forLocations(locations: List): SomeLatLngBounds
}
И классы для маркеров.
SomeMarker
— собственно маркер:
abstract class SomeMarker {
abstract fun remove()
}
SomeMarkerOptions
— для указания иконки и местоположения маркера.
data class SomeMarkerOptions(
val icon: Bitmap,
val position: Location
)
SomeClusterItem
— для маркера при кластеризации.
interface SomeClusterItem {
fun getLocation(): Location
fun getTitle(): String?
fun getSnippet(): String?
fun getDrawableResourceId(): Int
}
SomeCluster
— для кластера маркеров.
data class SomeCluster(
val location: Location,
val items: List
)
SelectableMarkerRenderer
нужен для возможности выделять маркеры при нажатии, меняя им иконку и сохраняя выбранный маркер.
interface SelectableMarkerRenderer- {
val pinBitmapDescriptorsCache: Map
var selectedItem: Item?
fun selectItem(item: Item?)
fun getVectorResourceAsBitmap(@DrawableRes vectorResourceId: Int): Bitmap
}
Также мы хотим иметь возможность сложной настройки внешнего вида маркера. Например генерируя иконку для него из разметки. Для этого скопируем класс из гугловой библиотеки — IconGenerator
:
/**
* Not full copy of com.google.maps.android.ui.IconGenerator
*/
class IconGenerator(private val context: Context) {
private val mContainer = LayoutInflater.from(context)
.inflate(R.layout.map_marker_view, null as ViewGroup?) as ViewGroup
private var mTextView: TextView?
private var mContentView: View?
init {
mTextView = mContainer.findViewById(R.id.amu_text) as TextView
mContentView = mTextView
}
fun makeIcon(text: CharSequence?): Bitmap {
if (mTextView != null) {
mTextView!!.text = text
}
return this.makeIcon()
}
fun makeIcon(): Bitmap {
val measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
mContainer.measure(measureSpec, measureSpec)
val measuredWidth = mContainer.measuredWidth
val measuredHeight = mContainer.measuredHeight
mContainer.layout(0, 0, measuredWidth, measuredHeight)
val r = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)
r.eraseColor(0)
val canvas = Canvas(r)
mContainer.draw(canvas)
return r
}
fun setContentView(contentView: View?) {
mContainer.removeAllViews()
mContainer.addView(contentView)
mContentView = contentView
val view = mContainer.findViewById(R.id.amu_text)
mTextView = if (view is TextView) view else null
}
fun setBackground(background: Drawable?) {
mContainer.setBackgroundDrawable(background)
if (background != null) {
val rect = Rect()
background.getPadding(rect)
mContainer.setPadding(rect.left, rect.top, rect.right, rect.bottom)
} else {
mContainer.setPadding(0, 0, 0, 0)
}
}
fun setContentPadding(left: Int, top: Int, right: Int, bottom: Int) {
mContentView!!.setPadding(left, top, right, bottom)
}
}
Создаём реализации нашей абстрактной карты
Наконец приступаем к переопределению созданных нами абстрактных классов.
Подключим библиотеки:
//google maps
googleImplementation 'com.google.android.gms:play-services-location:17.0.0'
googleImplementation 'com.google.maps.android:android-maps-utils-sdk-v3-compat:0.1' //clasterization
//huawei maps
huaweiImplementation 'com.huawei.hms:maps:4.0.1.302'
Также добавляем необходимое для карт разрешение в манифест. Для этого создайте ещё один файл манифеста (AndroidManifest.xml
) в папке src/huawei/
с таким содержимым:
Вот так будет выглядеть реализация карт для гугл. Добавляем в папку src/google/kotlin/com/example
класс MapViewImpl
:
class MapViewImpl : MapView {
private lateinit var mapView: com.google.android.libraries.maps.MapView
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun inflateMapViewImpl() {
mapView = com.google.android.libraries.maps.MapView(
context,
GoogleMapOptions().liteMode(liteModeEnabled).mapType(mapType.value)
)
addView(mapView)
}
override fun getMapAsync(function: (SomeMap) -> Unit) {
mapView.getMapAsync { function(SomeMapImpl(it)) }
}
override fun onCreate(mapViewBundle: Bundle?) {
mapView.onCreate(mapViewBundle)
}
override fun onStart() {
mapView.onStart()
}
override fun onResume() {
mapView.onResume()
}
override fun onPause() {
mapView.onPause()
}
override fun onStop() {
mapView.onStop()
}
override fun onLowMemory() {
mapView.onLowMemory()
}
override fun onDestroy() {
mapView.onDestroy()
}
override fun onSaveInstanceState(mapViewBundle: Bundle?) {
mapView.onSaveInstanceState(mapViewBundle)
}
/**
* We need to manually pass touch events to MapView
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
mapView.onTouchEvent(event)
return true
}
/**
* We need to manually pass touch events to MapView
*/
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
mapView.dispatchTouchEvent(event)
return true
}
}
А в папку src/huawei/kotlin/com/example
аналогичный класс MapViewImpl
, но уже с использование карт от Huawei:
class MapViewImpl : MapView {
private lateinit var mapView: com.huawei.hms.maps.MapView
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun inflateMapViewImpl() {
mapView = com.huawei.hms.maps.MapView(
context,
HuaweiMapOptions().liteMode(liteModeEnabled).mapType(mapType.value)
)
addView(mapView)
}
override fun getMapAsync(function: (SomeMap) -> Unit) {
mapView.getMapAsync { function(SomeMapImpl(it)) }
}
override fun onCreate(mapViewBundle: Bundle?) {
mapView.onCreate(mapViewBundle)
}
override fun onStart() {
mapView.onStart()
}
override fun onResume() {
mapView.onResume()
}
override fun onPause() {
try {
mapView.onPause()
} catch (e: Exception) {
// there can be ClassCastException: com.exmaple.App cannot be cast to android.app.Activity
// at com.huawei.hms.maps.MapView$MapViewLifecycleDelegate.onPause(MapView.java:348)
Log.wtf("MapView", "Error while pausing MapView", e)
}
}
override fun onStop() {
mapView.onStop()
}
override fun onLowMemory() {
mapView.onLowMemory()
}
override fun onDestroy() {
mapView.onDestroy()
}
override fun onSaveInstanceState(mapViewBundle: Bundle?) {
mapView.onSaveInstanceState(mapViewBundle)
}
/**
* We need to manually pass touch events to MapView
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
mapView.onTouchEvent(event)
return true
}
/**
* We need to manually pass touch events to MapView
*/
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
mapView.dispatchTouchEvent(event)
return true
}
}
Тут надо обратить внимание на 3 момента:
- Вьюху карты мы создаём программно, а не загружаем из разметки, т.к. только так можно передать в неё опции (лёгкий режим, тип карты etc).
- Переопределены
onTouchEvent
иdispatchTouchEvent
, с прокидывание вызовов вmapView
— без этого карты не будут реагировать на касания. - В реализации для Huawei был обнаружен крэш при приостановке карты в методе
onPause
, пришлось в try-catch обернуть. Надеюсь это поправят в обновлениях библиотеки)
Реализуем дополнительные абстракции
А теперь самое сложное. У нас в приложении было достаточно много кода для отображения, кастомизации и обработки нажатия на маркеры и кластеры маркеров. Когда начали это всё пытаться заабстрагировать — возникли сложности. Почти сразу выяснилось, что хотя в картах от Huawei есть кластеризация, она не полностью аналогична по функционалу кластеризации от гугла. Например нельзя влиять на внешний вид кластера и обрабатывать нажатия на него. Также в Huawei картах внешний вид отдельных маркеров (и обработка их событий) работает также как и маркеры, которые должны кластеризироваться. А вот в гугло-картах для кластеризующихся маркеров всё иначе — отдельная обработка событий, отдельный способ настройки внешнего вида и вообще всё это сделано в рамках отдельной библиотеки. В итоге пришлось думать как переписать код так, чтобы и сохранить функционал для гугло-карт и чтобы карты от Huawei работали.
В общем, пришли в итоге к такому варианту: создаём метод для показа множества маркеров, которые должны кластеризоваться, в него передаём нужные нам слушатели событий и возвращаем лямбду, для функционала выбора маркера. Вот реализация SomeMap
для гугло-карт:
class SomeMapImpl(val map: GoogleMap) : SomeMap() {
override fun setUiSettings(
isMapToolbarEnabled: Boolean?,
isCompassEnabled: Boolean?,
isRotateGesturesEnabled: Boolean?,
isMyLocationButtonEnabled: Boolean?,
isZoomControlsEnabled: Boolean?
) {
map.uiSettings.apply {
isMapToolbarEnabled?.let {
this.isMapToolbarEnabled = isMapToolbarEnabled
}
isCompassEnabled?.let {
this.isCompassEnabled = isCompassEnabled
}
isRotateGesturesEnabled?.let {
this.isRotateGesturesEnabled = isRotateGesturesEnabled
}
isMyLocationButtonEnabled?.let {
this.isMyLocationButtonEnabled = isMyLocationButtonEnabled
}
isZoomControlsEnabled?.let {
this.isZoomControlsEnabled = isZoomControlsEnabled
}
setAllGesturesEnabled(true)
}
}
override fun animateCamera(someCameraUpdate: SomeCameraUpdate) {
someCameraUpdate.toCameraUpdate()?.let { map.animateCamera(it) }
}
override fun moveCamera(someCameraUpdate: SomeCameraUpdate) {
someCameraUpdate.toCameraUpdate()?.let { map.moveCamera(it) }
}
override fun setOnCameraIdleListener(function: () -> Unit) {
map.setOnCameraIdleListener { function() }
}
override fun setOnMarkerClickListener(function: (SomeMarker) -> Boolean) {
map.setOnMarkerClickListener { function(MarkerImpl(it)) }
}
override fun setOnMapClickListener(function: () -> Unit) {
map.setOnMapClickListener { function() }
}
override fun setOnCameraMoveStartedListener(onCameraMoveStartedListener: (Int) -> Unit) {
map.setOnCameraMoveStartedListener { onCameraMoveStartedListener(it) }
}
override fun addMarker(markerOptions: SomeMarkerOptions): SomeMarker {
return MarkerImpl(
map.addMarker(
MarkerOptions()
.position(markerOptions.position.toLatLng())
.icon(BitmapDescriptorFactory.fromBitmap(markerOptions.icon))
)
)
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
map.setPadding(left, top, right, bottom)
}
override fun setOnCameraMoveListener(function: () -> Unit) {
map.setOnCameraMoveListener { function() }
}
override fun - addMarkers(
context: Context,
markers: List
- ,
clusterItemClickListener: (Item) -> Boolean,
clusterClickListener: (SomeCluster
- ) -> Boolean,
generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)?
): (Item?) -> Unit {
val clusterManager = ClusterManager
>(context, map)
.apply {
setOnClusterItemClickListener {
clusterItemClickListener(it.someClusterItem)
}
setOnClusterClickListener { cluster ->
val position = Location(cluster.position.latitude, cluster.position.longitude)
val items: List- = cluster.items.map { it.someClusterItem }
val someCluster: SomeCluster
- = SomeCluster(position, items)
clusterClickListener(someCluster)
}
}
map.setOnCameraIdleListener(clusterManager)
map.setOnMarkerClickListener(clusterManager)
val renderer =
object :
DefaultClusterRenderer
>(context, map, clusterManager),
SelectableMarkerRenderer> {
override val pinBitmapDescriptorsCache = mutableMapOf()
override var selectedItem: SomeClusterItemImpl- ? = null
override fun onBeforeClusterItemRendered(
item: SomeClusterItemImpl
- ,
markerOptions: MarkerOptions
) {
val icon = generateClusterItemIconFun
?.invoke(item.someClusterItem, item == selectedItem)
?: getVectorResourceAsBitmap(
item.someClusterItem.getDrawableResourceId(item == selectedItem)
)
markerOptions
.icon(BitmapDescriptorFactory.fromBitmap(icon))
.zIndex(1.0f) // to hide cluster pin under the office pin
}
override fun getColor(clusterSize: Int): Int {
return context.resources.color(R.color.primary)
}
override fun selectItem(item: SomeClusterItemImpl
- ?) {
selectedItem?.let {
val icon = generateClusterItemIconFun
?.invoke(it.someClusterItem, false)
?: getVectorResourceAsBitmap(
it.someClusterItem.getDrawableResourceId(false)
)
getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))
}
selectedItem = item
item?.let {
val icon = generateClusterItemIconFun
?.invoke(it.someClusterItem, true)
?: getVectorResourceAsBitmap(
it.someClusterItem.getDrawableResourceId(true)
)
getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))
}
}
override fun getVectorResourceAsBitmap(@DrawableRes vectorResourceId: Int): Bitmap {
return pinBitmapDescriptorsCache[vectorResourceId]
?: context.resources.generateBitmapFromVectorResource(vectorResourceId)
.also { pinBitmapDescriptorsCache[vectorResourceId] = it }
}
}
clusterManager.renderer = renderer
clusterManager.clearItems()
clusterManager.addItems(markers.map { SomeClusterItemImpl(it) })
clusterManager.cluster()
@Suppress("UnnecessaryVariable")
val pinItemSelectedCallback = fun(item: Item?) {
renderer.selectItem(item?.let { SomeClusterItemImpl(it) })
}
return pinItemSelectedCallback
}
}
fun Location.toLatLng() = LatLng(latitude, longitude)
fun SomeLatLngBounds.toLatLngBounds() = LatLngBounds(southwest?.toLatLng(), northeast?.toLatLng())
fun SomeCameraUpdate.toCameraUpdate(): CameraUpdate? {
return if (zoom != null) {
CameraUpdateFactory.newCameraPosition(
CameraPosition.fromLatLngZoom(
location?.toLatLng()
?: Location.DEFAULT_LOCATION.toLatLng(),
zoom
)
)
} else if (bounds != null && width != null && height != null && padding != null) {
CameraUpdateFactory.newLatLngBounds(
bounds.toLatLngBounds(),
width,
height,
padding
)
} else {
null
}
}
Самое сложное, как уже и говорилось — в addMarkers
методе. В нём используются ClusterManager
и ClusterRenderer
, аналогов которых нет в Huawei картах. К тому же, эти классы требуют, чтобы объекты, из которых будут создаваться маркеты для кластеризации реализовывали интерфейс ClusterItem
, аналога которому также нет у Huawei. В итоге пришлось изворачиваться и комбинировать наследование с инкапсуляцией. Data классы в проекте будут реализовывать наш интерфейс SomeClusterItem
, а гугловый интерфейс ClusterItem
будет реализовывать обёртка над классом с данными маркера. Вот такая:
data class SomeClusterItemImpl(
val someClusterItem: T
) : ClusterItem, SomeClusterItem {
override fun getSnippet(): String {
return someClusterItem.getSnippet() ?: ""
}
override fun getTitle(): String {
return someClusterItem.getTitle() ?: ""
}
override fun getPosition(): LatLng {
return someClusterItem.getLocation().toLatLng()
}
override fun getLocation(): Location {
return someClusterItem.getLocation()
}
}
В итоге, снаружи мы будем использовать библиотеко-независимый интерфейс, а внутри карт для гугла будем оборачивать его экземпляры в класс, реализующий ClusterItem
из гугловой библиотеки. Подробнее — смотрите реализацию addMarkers
выше.
Чтобы всё это работало, осталось только вот эти классы добавить:
class SomeLatLngBoundsImpl(bounds: LatLngBounds? = null) :
SomeLatLngBounds(bounds?.southwest?.toLocation(), bounds?.northeast?.toLocation()) {
override fun forLocations(locations: List): SomeLatLngBounds {
val bounds = LatLngBounds.builder()
.apply { locations.map { it.toLatLng() }.forEach { include(it) } }
.build()
return SomeLatLngBoundsImpl(bounds)
}
}
fun LatLng.toLocation(): Location {
return Location(latitude, longitude)
}
class MarkerImpl(private val marker: Marker?) : SomeMarker() {
override fun remove() {
marker?.remove()
}
}
С реализацией для Huawei будет проще — не надо возиться с оборачиванием SomeClusterItem
. Вот все классы, которые надо положить в src/huawei/kotlin/com/example
:
Реализация SomeMap
:
class SomeMapImpl(val map: HuaweiMap) : SomeMap() {
override fun setUiSettings(
isMapToolbarEnabled: Boolean?,
isCompassEnabled: Boolean?,
isRotateGesturesEnabled: Boolean?,
isMyLocationButtonEnabled: Boolean?,
isZoomControlsEnabled: Boolean?
) {
map.uiSettings.apply {
isMapToolbarEnabled?.let {
this.isMapToolbarEnabled = isMapToolbarEnabled
}
isCompassEnabled?.let {
this.isCompassEnabled = isCompassEnabled
}
isRotateGesturesEnabled?.let {
this.isRotateGesturesEnabled = isRotateGesturesEnabled
}
isMyLocationButtonEnabled?.let {
this.isMyLocationButtonEnabled = isMyLocationButtonEnabled
}
isZoomControlsEnabled?.let {
this.isZoomControlsEnabled = isZoomControlsEnabled
}
setAllGesturesEnabled(true)
}
}
override fun animateCamera(someCameraUpdate: SomeCameraUpdate) {
someCameraUpdate.toCameraUpdate()?.let { map.animateCamera(it) }
}
override fun moveCamera(someCameraUpdate: SomeCameraUpdate) {
someCameraUpdate.toCameraUpdate()?.let { map.moveCamera(it) }
}
override fun setOnCameraIdleListener(function: () -> Unit) {
map.setOnCameraIdleListener { function() }
}
override fun setOnMarkerClickListener(function: (SomeMarker) -> Boolean) {
map.setOnMarkerClickListener { function(MarkerImpl(it)) }
}
override fun setOnMapClickListener(function: () -> Unit) {
map.setOnMapClickListener { function() }
}
override fun setOnCameraMoveStartedListener(onCameraMoveStartedListener: (Int) -> Unit) {
map.setOnCameraMoveStartedListener { onCameraMoveStartedListener(it) }
}
override fun addMarker(markerOptions: SomeMarkerOptions): SomeMarker {
return MarkerImpl(
map.addMarker(
MarkerOptions()
.position(markerOptions.position.toLatLng())
.icon(BitmapDescriptorFactory.fromBitmap(markerOptions.icon))
)
)
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
map.setPadding(left, top, right, bottom)
}
override fun setOnCameraMoveListener(function: () -> Unit) {
map.setOnCameraMoveListener { function() }
}
override fun - addMarkers(
context: Context,
markers: List
- ,
clusterItemClickListener: (Item) -> Boolean,
clusterClickListener: (SomeCluster
- ) -> Boolean,
generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)?
): (Item?) -> Unit {
val addedMarkers = mutableListOf
>()
val selectableMarkerRenderer = object : SelectableMarkerRenderer- {
override val pinBitmapDescriptorsCache = mutableMapOf
()
override var selectedItem: Item? = null
override fun selectItem(item: Item?) {
selectedItem?.let {
val icon = generateClusterItemIconFun
?.invoke(it, false)
?: getVectorResourceAsBitmap(it.getDrawableResourceId(false))
getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))
}
selectedItem = item
item?.let {
val icon = generateClusterItemIconFun
?.invoke(it, true)
?: getVectorResourceAsBitmap(
it.getDrawableResourceId(true)
)
getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))
}
}
private fun getMarker(item: Item): Marker? {
return addedMarkers.firstOrNull { it.first == item }?.second
}
override fun getVectorResourceAsBitmap(@DrawableRes vectorResourceId: Int): Bitmap {
return pinBitmapDescriptorsCache[vectorResourceId]
?: context.resources.generateBitmapFromVectorResource(vectorResourceId)
.also { pinBitmapDescriptorsCache[vectorResourceId] = it }
}
}
addedMarkers += markers.map {
val selected = selectableMarkerRenderer.selectedItem == it
val icon = generateClusterItemIconFun
?.invoke(it, selected)
?: selectableMarkerRenderer.getVectorResourceAsBitmap(it.getDrawableResourceId(selected))
val markerOptions = MarkerOptions()
.position(it.getLocation().toLatLng())
.icon(BitmapDescriptorFactory.fromBitmap(icon))
.clusterable(true)
val marker = map.addMarker(markerOptions)
it to marker
}
map.setMarkersClustering(true)
map.setOnMarkerClickListener { clickedMarker ->
val clickedItem = addedMarkers.firstOrNull { it.second == clickedMarker }?.first
clickedItem?.let { clusterItemClickListener(it) } ?: false
}
return selectableMarkerRenderer::selectItem
}
}
fun Location.toLatLng() = LatLng(latitude, longitude)
fun SomeLatLngBounds.toLatLngBounds() = LatLngBounds(southwest?.toLatLng(), northeast?.toLatLng())
fun SomeCameraUpdate.toCameraUpdate(): CameraUpdate? {
return if (zoom != null) {
CameraUpdateFactory.newCameraPosition(
CameraPosition.fromLatLngZoom(
location?.toLatLng()
?: Location.DEFAULT_LOCATION.toLatLng(),
zoom
)
)
} else if (bounds != null && width != null && height != null && padding != null) {
CameraUpdateFactory.newLatLngBounds(
bounds.toLatLngBounds(),
width,
height,
padding
)
} else {
null
}
}
class SomeLatLngBoundsImpl(bounds: LatLngBounds? = null) :
SomeLatLngBounds(bounds?.southwest?.toLocation(), bounds?.northeast?.toLocation()) {
override fun forLocations(locations: List): SomeLatLngBounds {
val bounds = LatLngBounds.builder()
.apply { locations.map { it.toLatLng() }.forEach { include(it) } }
.build()
return SomeLatLngBoundsImpl(bounds)
}
}
fun LatLng.toLocation(): Location {
return Location(latitude, longitude)
}
class MarkerImpl(private val marker: Marker?) : SomeMarker() {
override fun remove() {
marker?.remove()
}
}
На этом реализацию наших абстракций мы закончили. Осталось показать, как это в коде будет использоваться. Важно иметь в виду, что в отличии от аналитики и геолокации, которые работают на любом девайсе, на котором установлены Huawei Mobile Services, карты будут работать только на устройствах от Huawei.
Используем нашу абстрактную карту
Итак, в разметку мы добавляем MapViewImpl
, как было показано выше и переходим к коду. Для начала нам надо из нашей MapView
получить объект карты:
mapView.getMapAsync { onMapReady(it) }
Когда она будет получена — будем рисовать на ней маркеры с помощью нашей абстракции. А также, при нажатии, выделять их и отображать сообщение. И ещё обрабатывать нажатие на кластер. При этом мы, как и планировалось, не зависим от реализации карт:
private fun onMapReady(map: SomeMap) {
map.setUiSettings(isMapToolbarEnabled = false, isCompassEnabled = false)
var pinItemSelected: ((MarkerItem?) -> Unit)? = null
fun onMarkerSelected(selectedMarkerItem: MarkerItem?) {
pinItemSelected?.invoke(selectedMarkerItem)
selectedMarkerItem?.let {
map.animateCamera(SomeCameraUpdate(it.getLocation(), DEFAULT_ZOOM))
Snackbar.make(root, "Marker selected: ${it.markerTitle}", Snackbar.LENGTH_SHORT).show()
}
}
with(map) {
setOnMapClickListener {
onMarkerSelected(null)
}
setOnCameraMoveStartedListener { reason ->
if (reason == SomeMap.REASON_GESTURE) {
onMarkerSelected(null)
}
}
}
locationGateway.requestLastLocation()
.flatMap { mapMarkersGateway.getMapMarkers(it) }
.subscribeBy { itemList ->
pinItemSelected = map.addMarkers(
requireContext(),
itemList.map { it },
{
onMarkerSelected(it)
true
},
{ someCluster ->
mapView?.let { mapViewRef ->
val bounds = SomeLatLngBoundsImpl()
.forLocations(someCluster.items.map { it.getLocation() })
val someCameraUpdate = SomeCameraUpdate(
bounds = bounds,
width = mapViewRef.width,
height = mapViewRef.height,
padding = 32.dp()
)
map.animateCamera(someCameraUpdate)
}
onMarkerSelected(null)
true
}
)
}
}
Часть кода, понятно, опущена для краткости. Полный пример вы можете найти на GitHub.
А вот как выглядят карты разных реализаций (сначала Huawei, потом Google):
По итогу работы с картами можно сказать следующее — с картами гораздо сложнее, чем с местоположением и аналитикой. Особенно, если есть маркеры и кластеризация. Хотя могло быть и хуже, конечно, если бы API для работы с картами отличалось сильнее. Так что можно сказать спасибо команде Huawei за облегчение поддержки карт их реализации.
Заключение
Рынок мобильных приложений меняется. Ещё вчера казавшиеся незыблемыми монополии Google и Apple наконец-то замечены не только пострадавшими от них разработчиками (из самых известных последних — Telegram, Microsoft, Epic Games), но и государственными структурами уровня EC и США. Надеюсь, на этом фоне наконец-то появится здоровая конкурентная среда хотя бы на Android. Работа Huawei в этой области радует — видно, что люди стараются максимально упростить жизнь разработчикам. После опыта общения с тех. поддержкой GooglePlay, где тебе отвечают роботы в основном (и они же и банят) в Huawei тебе отвечают люди. Мало того — когда у меня возник вопрос по их магазину приложений — у меня была возможность просто взять и позвонить живому человеку из Москвы и быстро решить проблему.
В общем, если вы хотите расширить свою аудиторию и получить качественную тех.поддержку в процессе — идите в Huawei. Чем больше там будет разработчиков, тем выше шанс, что и в GooglePlay что-то поменяется в лучшую сторону. И выиграют все.
Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка.