Встраиваем карты от Huawei в Android приложение

от автора

image

В предыдущих статьях мы создавали аккаунт разработчика для использования Huawei Mobile Services и подготавливали проект к их использованию. Потом использовали аналитику от Huawei вместо аналога от Google. Также поступили и с определением геолокации. В этой же статье мы будем использовать карты от Huawei вместо карт от Google.

Вот полный список статей из цикла:

  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. тык
  2. Встраиваем Huawei Analytics. тык
  3. Используем геолокацию от Huawei. тык
  4. 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 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/ с таким содержимым:

<manifest xmlns:android="http://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 момента:

  1. Вьюху карты мы создаём программно, а не загружаем из разметки, т.к. только так можно передать в неё опции (лёгкий режим, тип карты etc).
  2. Переопределены onTouchEvent и dispatchTouchEvent, с прокидывание вызовов в mapView — без этого карты не будут реагировать на касания.
  3. В реализации для 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.

А вот как выглядят карты разных реализаций (сначала Huawei, потом Google):

Huawei maps

Google maps

По итогу работы с картами можно сказать следующее — с картами гораздо сложнее, чем с местоположением и аналитикой. Особенно, если есть маркеры и кластеризация. Хотя могло быть и хуже, конечно, если бы API для работы с картами отличалось сильнее. Так что можно сказать спасибо команде Huawei за облегчение поддержки карт их реализации.

Заключение

Рынок мобильных приложений меняется. Ещё вчера казавшиеся незыблемыми монополии Google и Apple наконец-то замечены не только пострадавшими от них разработчиками (из самых известных последних — Telegram, Microsoft, Epic Games) но и государственными структурами уровня EC и США. Надеюсь, на этом фоне наконец-то появится здоровая конкурентная среда хотя бы на Android. Работа Huawei в этой области радует — видно, что люди стараются максимально упростить жизнь разработчикам. После опыта общения с тех. поддержкой GooglePlay, где тебе отвечают роботы в основном (и они же и банят) в Huawei тебе отвечают люди. Мало того — когда у меня возник вопрос по их магазину приложений — у меня была возможность просто взять и позвонить живому человеку из Москвы и быстро решить проблему.

В общем, если вы хотите расширить свою аудиторию и получить качественную тех.поддержку в процессе — идите в Huawei. Чем больше там будет разработчиков, тем выше шанс, что и в GooglePlay что-то поменяется в лучшую сторону. И выиграют все.

Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка.

ссылка на оригинал статьи https://habr.com/ru/post/521810/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *