Map module: review comments and small fixes

This commit is contained in:
Grigorii 2022-10-20 12:03:14 +04:00
parent c8c44ede4f
commit 3f44ad3ba7
8 changed files with 76 additions and 87 deletions

View File

@ -43,7 +43,7 @@ abstract class AbstractMapManager<TMapView : View, TMap : Any, TLocation : Any>(
abstract fun increaseZoom(target: TLocation, zoomIncreaseValue: Int = getDefaultZoomStep()) abstract fun increaseZoom(target: TLocation, zoomIncreaseValue: Int = getDefaultZoomStep())
abstract fun decreaseZoom(target: TLocation, zoomIncreaseValue: Int = getDefaultZoomStep()) abstract fun decreaseZoom(target: TLocation, zoomDecreaseValue: Int = getDefaultZoomStep())
abstract fun setMapAllGesturesEnabled(enabled: Boolean) abstract fun setMapAllGesturesEnabled(enabled: Boolean)

View File

@ -17,28 +17,24 @@ open class GoogleIconGenerator<T : ClusterItem>(
private val context: Context private val context: Context
) : IconGenerator(context), BaseIconGenerator<T, Cluster<T>, BitmapDescriptor> { ) : IconGenerator(context), BaseIconGenerator<T, Cluster<T>, BitmapDescriptor> {
init {
setBackground(ContextCompat.getDrawable(context, R.drawable.default_cluster_background))
LayoutInflater
.from(context)
.inflate(R.layout.view_google_map_cluster_item, null)
.apply {
setContentView(this)
}
}
private val clusterIconsCache = SparseArray<BitmapDescriptor>() private val clusterIconsCache = SparseArray<BitmapDescriptor>()
private val clusterItemIconsCache = mutableMapOf<T, BitmapDescriptor>() private val clusterItemIconsCache = mutableMapOf<T, BitmapDescriptor>()
fun setDefaultViewAndBackground() {
val defaultLayout = LayoutInflater.from(context).inflate(R.layout.view_google_map_cluster_item, null)
setBackground(ContextCompat.getDrawable(context, R.drawable.default_cluster_background))
setContentView(defaultLayout)
}
override fun getClusterIcon(cluster: Cluster<T>): BitmapDescriptor? { override fun getClusterIcon(cluster: Cluster<T>): BitmapDescriptor? {
val clusterSize = cluster.size val clusterSize = cluster.size
return BitmapDescriptorFactory.fromBitmap(makeIcon(clusterSize.toString())) return BitmapDescriptorFactory.fromBitmap(makeIcon(clusterSize.toString()))
} }
override fun getClusterItemIcon(clusterItem: T): BitmapDescriptor? = override fun getClusterItemIcon(clusterItem: T): BitmapDescriptor? {
context.getDrawable(ru.touchin.basemap.R.drawable.marker_default_icon)?.let { val defaultIcon = context.getDrawable(ru.touchin.basemap.R.drawable.marker_default_icon)
BitmapDescriptorFactory.fromBitmap(it.toBitmap()) return BitmapDescriptorFactory.fromBitmap(defaultIcon?.toBitmap())
} }
override fun getClusterItemView(clusterItem: T): BitmapDescriptor? = override fun getClusterItemView(clusterItem: T): BitmapDescriptor? =
clusterItemIconsCache.getOrPutIfNotNull(clusterItem) { getClusterItemIcon(clusterItem) } clusterItemIconsCache.getOrPutIfNotNull(clusterItem) { getClusterItemIcon(clusterItem) }

View File

@ -1,7 +1,6 @@
package ru.touchin.googlemap package ru.touchin.googlemap
import android.content.Context import android.content.Context
import android.view.LayoutInflater
import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.MarkerOptions import com.google.android.gms.maps.model.MarkerOptions
@ -19,10 +18,7 @@ open class GoogleMapItemRenderer<TClusterItem : ClusterItem>(
) : DefaultClusterRenderer<TClusterItem>(context, googleMap, clusterManager) { ) : DefaultClusterRenderer<TClusterItem>(context, googleMap, clusterManager) {
var iconGenerator: BaseIconGenerator<TClusterItem, Cluster<TClusterItem>, BitmapDescriptor> = var iconGenerator: BaseIconGenerator<TClusterItem, Cluster<TClusterItem>, BitmapDescriptor> =
GoogleIconGenerator<TClusterItem>(context).apply { GoogleIconGenerator<TClusterItem>(context).apply { setDefaultViewAndBackground() }
setBackground(context.getDrawable(R.drawable.default_cluster_background))
setContentView(LayoutInflater.from(context).inflate(R.layout.view_google_map_cluster_item, null))
}
override fun shouldRenderAsCluster(cluster: Cluster<TClusterItem>): Boolean = override fun shouldRenderAsCluster(cluster: Cluster<TClusterItem>): Boolean =
cluster.size > minClusterItemSize cluster.size > minClusterItemSize

View File

@ -20,7 +20,7 @@ class GoogleMapManager(mapView: MapView) : AbstractMapManager<MapView, GoogleMap
override fun initialize(mapListener: AbstractMapListener<MapView, GoogleMap, LatLng>?) { override fun initialize(mapListener: AbstractMapListener<MapView, GoogleMap, LatLng>?) {
super.initialize(mapListener) super.initialize(mapListener)
mapView.getMapAsync(::initMap) mapView.getMapAsync(this::initMap)
} }
override fun initMap(map: GoogleMap) { override fun initMap(map: GoogleMap) {

View File

@ -12,18 +12,20 @@ import com.google.maps.android.clustering.algo.Algorithm
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
@OptIn(FlowPreview::class)
class GooglePlacemarkManager<TClusterItem : ClusterItem>( class GooglePlacemarkManager<TClusterItem : ClusterItem>(
context: Context, context: Context,
private val lifecycleOwner: LifecycleOwner, private val lifecycleOwner: LifecycleOwner,
@ -47,7 +49,8 @@ class GooglePlacemarkManager<TClusterItem : ClusterItem>(
private val markers = mutableListOf<TClusterItem>() private val markers = mutableListOf<TClusterItem>()
private var lastVisibleItems = emptyList<TClusterItem>() private var lastVisibleItems = emptyList<TClusterItem>()
private var onCameraIdleListener: (() -> Unit)? = null
var onCameraIdleListener: (() -> Unit)? = null
var clusterRenderer: GoogleMapItemRenderer<TClusterItem>? = null var clusterRenderer: GoogleMapItemRenderer<TClusterItem>? = null
set(value) { set(value) {
@ -55,8 +58,6 @@ class GooglePlacemarkManager<TClusterItem : ClusterItem>(
setRenderer(value) setRenderer(value)
} }
private val lock = Any()
init { init {
googleMap.setOnCameraIdleListener(this) googleMap.setOnCameraIdleListener(this)
googleMap.setOnMarkerClickListener(this) googleMap.setOnMarkerClickListener(this)
@ -65,40 +66,28 @@ class GooglePlacemarkManager<TClusterItem : ClusterItem>(
setOnClusterItemClickListener(clusterItemTapAction) setOnClusterItemClickListener(clusterItemTapAction)
} }
fun setItems(items: Collection<TClusterItem>) { @Synchronized
synchronized(lock) {
markers.clear()
markers.addAll(items)
onDataChanged()
}
}
override fun addItems(items: Collection<TClusterItem>) { override fun addItems(items: Collection<TClusterItem>) {
synchronized(lock) { markers.addAll(items)
markers.addAll(items) onDataChanged()
onDataChanged()
}
} }
@Synchronized
override fun addItem(clusterItem: TClusterItem) { override fun addItem(clusterItem: TClusterItem) {
synchronized(lock) { markers.add(clusterItem)
markers.add(clusterItem) onDataChanged()
onDataChanged()
}
} }
@Synchronized
override fun removeItem(atmClusterItem: TClusterItem) { override fun removeItem(atmClusterItem: TClusterItem) {
synchronized(lock) { markers.remove(atmClusterItem)
markers.remove(atmClusterItem) onDataChanged()
onDataChanged()
}
} }
@Synchronized
override fun clearItems() { override fun clearItems() {
synchronized(lock) { markers.clear()
markers.clear() onDataChanged()
onDataChanged()
}
} }
override fun onCameraIdle() { override fun onCameraIdle() {
@ -106,22 +95,11 @@ class GooglePlacemarkManager<TClusterItem : ClusterItem>(
onCameraIdleEvent.tryEmit(true) onCameraIdleEvent.tryEmit(true)
} }
private fun onDataChanged() { @Synchronized
onVisibilityChangedEvent.tryEmit(getData()) fun setItems(items: Collection<TClusterItem>) {
} markers.clear()
markers.addAll(items)
private fun getData(): Pair<VisibleRegion?, List<TClusterItem>> = onDataChanged()
googleMap.projection.visibleRegion to markers
private fun listenToCameraIdleEvents() {
cameraIdleJob = lifecycleOwner.lifecycleScope.launchWhenStarted {
onCameraIdleEvent
.debounce(CAMERA_DEBOUNCE_MILLI)
.flowOn(Dispatchers.Main)
.collect {
onCameraIdleListener?.invoke()
}
}
} }
fun startClustering() { fun startClustering() {
@ -131,10 +109,7 @@ class GooglePlacemarkManager<TClusterItem : ClusterItem>(
.debounce(CLUSTERING_START_DEBOUNCE_MILLI) .debounce(CLUSTERING_START_DEBOUNCE_MILLI)
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.onStart { emit(getData()) } .onStart { emit(getData()) }
.filter { (visibleRegion, _) -> visibleRegion != null } .mapNotNull { (region, items) -> findItemsInRegion(region, items) }
.map { (visibleRegion, clusteringItems) ->
clusteringItems.filter { visibleRegion?.latLngBounds?.contains(it.position) == true }
}
.sample(CLUSTERING_NEW_LIST_CONSUMING_THROTTLE_MILLIS) .sample(CLUSTERING_NEW_LIST_CONSUMING_THROTTLE_MILLIS)
.catch { emit(lastVisibleItems) } .catch { emit(lastVisibleItems) }
.flowOn(Dispatchers.Main) .flowOn(Dispatchers.Main)
@ -148,16 +123,32 @@ class GooglePlacemarkManager<TClusterItem : ClusterItem>(
listenToCameraIdleEvents() listenToCameraIdleEvents()
} }
private fun listenToCameraIdleEvents() {
cameraIdleJob = lifecycleOwner.lifecycleScope.launchWhenStarted {
onCameraIdleEvent
.debounce(CAMERA_DEBOUNCE_MILLI)
.flowOn(Dispatchers.Main)
.collect {
onCameraIdleListener?.invoke()
}
}
}
fun stopClustering() { fun stopClustering() {
clusteringJob?.cancel() clusteringJob?.cancel()
cameraIdleJob?.cancel() cameraIdleJob?.cancel()
} }
fun setOnCameraIdleListener(listener: () -> Unit) { private fun onDataChanged() {
if (onCameraIdleListener != null) return onVisibilityChangedEvent.tryEmit(getData())
onCameraIdleListener = listener
} }
private fun getData(): Pair<VisibleRegion?, List<TClusterItem>> =
googleMap.projection.visibleRegion to markers
private fun findItemsInRegion(region: VisibleRegion?, items: List<TClusterItem>): List<TClusterItem>? =
region?.let { items.filter { item -> item.position in region.latLngBounds } }
private companion object { private companion object {
const val CAMERA_DEBOUNCE_MILLI = 50L const val CAMERA_DEBOUNCE_MILLI = 50L

View File

@ -8,11 +8,11 @@ import com.yandex.runtime.ui_view.ViewProvider
class DefaultIconGenerator<T : PointModel>(private val context: Context) : YandexIconGenerator<T>() { class DefaultIconGenerator<T : PointModel>(private val context: Context) : YandexIconGenerator<T>() {
override fun getClusterIcon(cluster: List<T>): ViewProvider { override fun getClusterIcon(cluster: List<T>): ViewProvider {
val view: TextView = LayoutInflater.from(context).inflate(R.layout.default_cluster_view, null) as TextView val textView = LayoutInflater.from(context).inflate(R.layout.default_cluster_view, null).apply {
view.setText(cluster.size.toString()) (this as? TextView)?.text = cluster.size.toString()
view.setBackgroundResource(ru.touchin.basemap.R.drawable.marker_default_icon) setBackgroundResource(ru.touchin.basemap.R.drawable.marker_default_icon)
}
return ViewProvider(view) return ViewProvider(textView)
} }
override fun getClusterItemIcon(clusterItem: T) = ViewProvider( override fun getClusterItemIcon(clusterItem: T) = ViewProvider(

View File

@ -135,10 +135,12 @@ class YandexMapManager(
} }
override fun setMapAllGesturesEnabled(enabled: Boolean) { override fun setMapAllGesturesEnabled(enabled: Boolean) {
map.isRotateGesturesEnabled = enabled with(map) {
map.isScrollGesturesEnabled = enabled isRotateGesturesEnabled = enabled
map.isTiltGesturesEnabled = enabled isScrollGesturesEnabled = enabled
map.isZoomGesturesEnabled = enabled isTiltGesturesEnabled = enabled
isZoomGesturesEnabled = enabled
}
} }
override fun setMyLocationEnabled(enabled: Boolean) { override fun setMyLocationEnabled(enabled: Boolean) {

View File

@ -15,6 +15,11 @@ class YandexPlacemarkManager<TPoint : PointModel>(
private val markerTapAction: (MapObject, Point) -> Boolean, private val markerTapAction: (MapObject, Point) -> Boolean,
) : ClusterListener, ClusterTapListener, MapObjectTapListener { ) : ClusterListener, ClusterTapListener, MapObjectTapListener {
private companion object {
const val DEFAULT_CLUSTER_RADIUS = 42.0
const val DEFAULT_MIN_ZOOM = 35
}
private val markers = mutableListOf<TPoint>() private val markers = mutableListOf<TPoint>()
private var placemarkCollection: ClusterizedPlacemarkCollection? = null private var placemarkCollection: ClusterizedPlacemarkCollection? = null
@ -25,8 +30,8 @@ class YandexPlacemarkManager<TPoint : PointModel>(
fun addMarkersOnMap( fun addMarkersOnMap(
mapView: MapView, mapView: MapView,
clusterRadius: Double = 42.0, clusterRadius: Double = DEFAULT_CLUSTER_RADIUS,
minZoom: Int = 35 minZoom: Int = DEFAULT_MIN_ZOOM
) { ) {
removeMarkers() removeMarkers()
@ -52,9 +57,8 @@ class YandexPlacemarkManager<TPoint : PointModel>(
} }
override fun onClusterAdded(cluster: Cluster) { override fun onClusterAdded(cluster: Cluster) {
mapItemRenderer.getClusterIcon(cluster.toPlacemarkList())?.let { val clusterIcon = mapItemRenderer.getClusterIcon(cluster.toPlacemarkList())
cluster.appearance.setView(it) clusterIcon?.let(cluster.appearance::setView)
}
cluster.addClusterTapListener(this) cluster.addClusterTapListener(this)
} }