[Java, Разработка мобильных приложений, Разработка под Android, Kotlin, Gradle] Встраиваем карты от Huawei в Android приложение

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
16-Окт-2020 14:30


В предыдущих статьях мы создавали аккаунт разработчика для использования 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 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 момента:
  • Вьюху карты мы создаём программно, а не загружаем из разметки, т.к. только так можно передать в неё опции (лёгкий режим, тип карты 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.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_java, #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_gradle, #_gradle, #_java, #_kotlin, #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_java, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
, #_razrabotka_pod_android (
Разработка под Android
)
, #_kotlin, #_gradle
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 05-Июн 18:14
Часовой пояс: UTC + 5