[Java, Разработка мобильных приложений, Разработка под Android, Kotlin, Gradle] Встраиваем карты от Huawei в Android приложение
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В предыдущих статьях мы создавали аккаунт разработчика для использования 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
===========
Похожие новости:
- [Разработка под Android, Тестирование мобильных приложений, Kotlin] Паттерн PageObject в Kotlin для UI-тестирования Android (перевод)
- [Java, Управление персоналом, Софт] How To Build a Password Management Software Using JAVA?
- [Java, Big Data] Distributed File Systems
- [JavaScript, ReactJS, Визуализация данных, Инфографика, Разработка веб-сайтов] Визуализация сложных данных с использованием D3 и React
- [JavaScript] Удобная платформа для подбора библиотек и фреймворков JavaScript — openbase (перевод)
- [Разработка под iOS, Разработка мобильных приложений, Xcode, Swift] Мой Covid-19 lockdown проект, или, как я полез в кастомный UICollectionViewLayout и получил ChatLayout
- [Gradle, Java, Kotlin, Разработка мобильных приложений, Разработка под Android] Встраиваем геолокацию от Huawei в Android приложение
- [Программирование, Java, Конференции, Микросервисы] 29 октября приглашаем на онлайн-митап Hot Java
- [Разработка мобильных приложений, Разработка под Android, Kotlin, Учебный процесс в IT] Android Academy Fundamentals: теперь прямо у тебя дома
- [Разработка веб-сайтов, JavaScript, VueJS] Отдаем корректный код 404 в связке VUE SPA + SSR
Теги для поиска: #_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
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:27
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В предыдущих статьях мы создавали аккаунт разработчика для использования Huawei Mobile Services и подготавливали проект к их использованию. Потом использовали аналитику от Huawei вместо аналога от Google. Также поступили и с определением геолокации. В этой же статье мы будем использовать карты от Huawei вместо карт от Google. Вот полный список статей из цикла:
В чём сложность К сожалению, с картами не получится так просто, как было с аналитикой и геолокацией. Что и неудивительно, т.к. это гораздо более сложная система сама по себе. И очень часто в приложениях карты и взаимодействие с ними кастомизируется. Например, отображают маркеры, кластеризуют их. Поэтому кода будет много, т.к. надо всё это заабстрагировать, имея в виду некоторые отличия в 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 момента:
Реализуем дополнительные абстракции А теперь самое сложное. У нас в приложении было достаточно много кода для отображения, кастомизации и обработки нажатия на маркеры и кластеры маркеров. Когда начали это всё пытаться заабстрагировать — возникли сложности. Почти сразу выяснилось, что хотя в картах от 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 =========== Похожие новости:
Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin, #_gradle |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:27
Часовой пояс: UTC + 5