Как стать автором
Обновить

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

Время на прочтение19 мин
Количество просмотров3.9K

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. Вот ссылка.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 11: ↑11 и ↓0+11
Комментарии4

Публикации

Истории

Работа

iOS разработчик
22 вакансии
Java разработчик
349 вакансий
Swift разработчик
25 вакансий

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн