Русский
Русский
English
Статистика
Реклама

Встраиваем карты от 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 mapsgoogleImplementation 'com.google.android.gms:play-services-location:17.0.0'googleImplementation 'com.google.maps.android:android-maps-utils-sdk-v3-compat:0.1' //clasterization//huawei mapshuaweiImplementation 'com.huawei.hms:maps:4.0.1.302'

Также добавляем необходимое для карт разрешение в манифест. Для этого создайте ещё один файл манифеста (AndroidManifest.xml) в папке src/huawei/ с таким содержимым:


<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    package="com.example">    <!-- used for MapKit -->    <uses-permission android:name="com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA"/></manifest>

Вот так будет выглядеть реализация карт для гугл. Добавляем в папку src/google/kotlin/com/example класс MapViewImpl:


class MapViewImpl : MapView {    private lateinit var mapView: com.google.android.libraries.maps.MapView    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(        context,        attrs,        defStyleAttr    )    override fun inflateMapViewImpl() {        mapView = com.google.android.libraries.maps.MapView(            context,            GoogleMapOptions().liteMode(liteModeEnabled).mapType(mapType.value)        )        addView(mapView)    }    override fun getMapAsync(function: (SomeMap) -> Unit) {        mapView.getMapAsync { function(SomeMapImpl(it)) }    }    override fun onCreate(mapViewBundle: Bundle?) {        mapView.onCreate(mapViewBundle)    }    override fun onStart() {        mapView.onStart()    }    override fun onResume() {        mapView.onResume()    }    override fun onPause() {        mapView.onPause()    }    override fun onStop() {        mapView.onStop()    }    override fun onLowMemory() {        mapView.onLowMemory()    }    override fun onDestroy() {        mapView.onDestroy()    }    override fun onSaveInstanceState(mapViewBundle: Bundle?) {        mapView.onSaveInstanceState(mapViewBundle)    }    /**     * We need to manually pass touch events to MapView     */    override fun onTouchEvent(event: MotionEvent?): Boolean {        mapView.onTouchEvent(event)        return true    }    /**     * We need to manually pass touch events to MapView     */    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {        mapView.dispatchTouchEvent(event)        return true    }}

А в папку src/huawei/kotlin/com/example аналогичный класс MapViewImpl но уже с использование карт от Huawei:


class MapViewImpl : MapView {    private lateinit var mapView: com.huawei.hms.maps.MapView    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(        context,        attrs,        defStyleAttr    )    override fun inflateMapViewImpl() {        mapView = com.huawei.hms.maps.MapView(            context,            HuaweiMapOptions().liteMode(liteModeEnabled).mapType(mapType.value)        )        addView(mapView)    }    override fun getMapAsync(function: (SomeMap) -> Unit) {        mapView.getMapAsync { function(SomeMapImpl(it)) }    }    override fun onCreate(mapViewBundle: Bundle?) {        mapView.onCreate(mapViewBundle)    }    override fun onStart() {        mapView.onStart()    }    override fun onResume() {        mapView.onResume()    }    override fun onPause() {        try {            mapView.onPause()        } catch (e: Exception) {            // there can be ClassCastException: com.exmaple.App cannot be cast to android.app.Activity            // at com.huawei.hms.maps.MapView$MapViewLifecycleDelegate.onPause(MapView.java:348)            Log.wtf("MapView", "Error while pausing MapView", e)        }    }    override fun onStop() {        mapView.onStop()    }    override fun onLowMemory() {        mapView.onLowMemory()    }    override fun onDestroy() {        mapView.onDestroy()    }    override fun onSaveInstanceState(mapViewBundle: Bundle?) {        mapView.onSaveInstanceState(mapViewBundle)    }    /**     * We need to manually pass touch events to MapView     */    override fun onTouchEvent(event: MotionEvent?): Boolean {        mapView.onTouchEvent(event)        return true    }    /**     * We need to manually pass touch events to MapView     */    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {        mapView.dispatchTouchEvent(event)        return true    }}

Тут надо обратить внимание на 3 момента:


  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: https://github.com/MobileUpLLC/huawei_and_google_services.


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


Huawei maps


Google maps


По итогу работы с картами можно сказать следующее с картами гораздо сложнее, чем с местоположением и аналитикой. Особенно, если есть маркеры и кластеризация. Хотя могло быть и хуже, конечно, если бы 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
К списку статей
Опубликовано: 16.10.2020 12:13:24
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Разработка мобильных приложений

Разработка под android

Kotlin

Gradle

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru