В предыдущих статьях мы создавали аккаунт разработчика для использования 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
вот
это:
<declare-styleable name="MapView"> <attr name="someMapType"> <enum name="none" value="0"/> <enum name="normal" value="1"/> <enum name="satellite" value="2"/> <enum name="terrain" value="3"/> <enum name="hybrid" value="4"/> </attr> <attr format="boolean" name="liteModeEnabled"/></declare-styleable>
Это нам позволит прямо в разметке, используя нашу абстрактную
карту передавать тип карты и нужен ли нам облегчённый режим для
неё. Выглядеть в разметке это будет как-то так (реализация
MapViewImpl
будет показана далее):
<com.example.ui.base.widget.map.MapViewImpl android:layout_width="match_parent" android:layout_height="150dp" app:liteModeEnabled="true" app:someMapType="normal"/>
Как можно заметить в коде нашего абстрактного класса
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 <Item : SomeClusterItem> addMarkers( context: Context, markers: List<Item>, clusterItemClickListener: (Item) -> Boolean, clusterClickListener: (SomeCluster<Item>) -> 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<Location>): 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<T : SomeClusterItem>( val location: Location, val items: List<T>)
SelectableMarkerRenderer
нужен для возможности
выделять маркеры при нажатии, меняя им иконку и сохраняя выбранный
маркер.
interface SelectableMarkerRenderer<Item : SomeClusterItem> { val pinBitmapDescriptorsCache: Map<Int, Bitmap> 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<View>(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 mapsgoogleImplementation '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 mapshuaweiImplementation 'com.huawei.hms:maps:4.0.1.302'
Также добавляем необходимое для карт разрешение в манифест. Для
этого создайте ещё один файл манифеста
(AndroidManifest.xml
) в папке src/huawei/
с таким содержимым:
<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android" package="com.example"> <!-- used for MapKit --> <uses-permission android:name="com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA"/></manifest>
Вот так будет выглядеть реализация карт для гугл. Добавляем в
папку 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 <Item : SomeClusterItem> addMarkers( context: Context, markers: List<Item>, clusterItemClickListener: (Item) -> Boolean, clusterClickListener: (SomeCluster<Item>) -> Boolean, generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)? ): (Item?) -> Unit { val clusterManager = ClusterManager<SomeClusterItemImpl<Item>>(context, map) .apply { setOnClusterItemClickListener { clusterItemClickListener(it.someClusterItem) } setOnClusterClickListener { cluster -> val position = Location(cluster.position.latitude, cluster.position.longitude) val items: List<Item> = cluster.items.map { it.someClusterItem } val someCluster: SomeCluster<Item> = SomeCluster(position, items) clusterClickListener(someCluster) } } map.setOnCameraIdleListener(clusterManager) map.setOnMarkerClickListener(clusterManager) val renderer = object : DefaultClusterRenderer<SomeClusterItemImpl<Item>>(context, map, clusterManager), SelectableMarkerRenderer<SomeClusterItemImpl<Item>> { override val pinBitmapDescriptorsCache = mutableMapOf<Int, Bitmap>() override var selectedItem: SomeClusterItemImpl<Item>? = null override fun onBeforeClusterItemRendered( item: SomeClusterItemImpl<Item>, 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<Item>?) { 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<T : SomeClusterItem>( 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<Location>): 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 <Item : SomeClusterItem> addMarkers( context: Context, markers: List<Item>, clusterItemClickListener: (Item) -> Boolean, clusterClickListener: (SomeCluster<Item>) -> Boolean, generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)? ): (Item?) -> Unit { val addedMarkers = mutableListOf<Pair<Item, Marker>>() val selectableMarkerRenderer = object : SelectableMarkerRenderer<Item> { override val pinBitmapDescriptorsCache = mutableMapOf<Int, Bitmap>() 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<Location>): 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: https://github.com/MobileUpLLC/huawei_and_google_services.
А вот как выглядят карты разных реализаций (сначала Huawei, потом Google):
По итогу работы с картами можно сказать следующее с картами гораздо сложнее, чем с местоположением и аналитикой. Особенно, если есть маркеры и кластеризация. Хотя могло быть и хуже, конечно, если бы API для работы с картами отличалось сильнее. Так что можно сказать спасибо команде Huawei за облегчение поддержки карт их реализации.
Заключение
Рынок мобильных приложений меняется. Ещё вчера казавшиеся незыблемыми монополии Google и Apple наконец-то замечены не только пострадавшими от них разработчиками (из самых известных последних Telegram, Microsoft, Epic Games) но и государственными структурами уровня EC и США. Надеюсь, на этом фоне наконец-то появится здоровая конкурентная среда хотя бы на Android. Работа Huawei в этой области радует видно, что люди стараются максимально упростить жизнь разработчикам. После опыта общения с тех. поддержкой GooglePlay, где тебе отвечают роботы в основном (и они же и банят) в Huawei тебе отвечают люди. Мало того когда у меня возник вопрос по их магазину приложений у меня была возможность просто взять и позвонить живому человеку из Москвы и быстро решить проблему.
В общем, если вы хотите расширить свою аудиторию и получить качественную тех.поддержку в процессе идите в Huawei. Чем больше там будет разработчиков, тем выше шанс, что и в GooglePlay что-то поменяется в лучшую сторону. И выиграют все.
Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка: https://github.com/MobileUpLLC/huawei_and_google_services.