Android Kaki

Build beautiful, usable products using required Components for Android.

Android MapView with clustered and dynamic photos in marker | by Roman Chugunov | April 2023


Animation in Google Maps marker
<meta-data
android:title="com.google.android.geo.API_KEY"
android:worth="YOUR_API_KEY" />
[
{
"lat": 59.92140394439577,
"lon": 30.445576954709395,
"icon": "1619152.jpg"
},
{
"lat": 59.93547541514004,
"lon": 30.21481515274267,
"icon": "1712315710.jpg"
},
class MapApplication : Application() {

private val context = CoroutineScope(Dispatchers.Default)

private val _dataFlow = MutableSharedFlow<List<MarkerData>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)

val dataFlow: Flow<List<MarkerData>> = _dataFlow

override fun onCreate() {
super.onCreate()

val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()

val service = retrofit.create(MarkerLocationsService::class.java)

context.launch {
_dataFlow.tryEmit(service.getLocations())
}
}
}

override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap

scope.launch {
(application as MapApplication).dataFlow.collect { data ->
mMap.clear()
data.forEach { marker ->
mMap.addMarker(MarkerOptions().position(LatLng(marker.lat, marker.lon)))
}
}
}
}

internal class BoundariesListener(
private val map: GoogleMap,
) : GoogleMap.OnCameraIdleListener {

private val _boundariesFlow = MutableSharedFlow<LatLngBounds>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val boundariesFlow: Flow<LatLngBounds> = _boundariesFlow

override fun onCameraIdle() {
val boundaries = map.projection.visibleRegion.latLngBounds
_boundariesFlow.tryEmit(boundaries)
}
}

override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap

val boundariesListener = BoundariesListener(googleMap)

mMap.setOnCameraMoveStartedListener(boundariesListener)
mMap.setOnCameraIdleListener(boundariesListener)

scope.launch {
(application as MapApplication).dataFlow.combine(boundariesListener.boundariesFlow) { data, boundaries ->
data to boundaries
}.collect { (data, boundaries) ->
mMap.clear()

data.filter { boundaries.bounds.includesLocation(it.lat, it.lon) }
.forEach { marker ->
mMap.addMarker(MarkerOptions().position(LatLng(marker.lat, marker.lon)))
}
}
}
}

fun LatLngBounds.includesLocation(lat: Double, lon: Double): Boolean {
return this.northeast.latitude > lat && this.southwest.latitude < lat &&
this.northeast.longitude > lon && this.southwest.longitude < lon

}

dependencies {

...

implementation 'com.google.maps.android:android-maps-utils:<version>'
}

data class MapMarker(
val titleText: String,
val location: LatLng,
) : ClusterItem {

override fun getPosition(): LatLng = location

override fun getTitle(): String? = null

override fun getSnippet(): String? = null
}

override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap

val boundariesListener = BoundariesListener(googleMap)

val clusterManager = ClusterManager<MapMarker>(this, mMap)
val mapRenderer = MapMarkersRenderer(
context = this,
callback = this,
map = mMap,
clusterManager = clusterManager
)
clusterManager.renderer = mapRenderer

class MapMarkersRenderer(
context: Context,
map: GoogleMap,
clusterManager: ClusterManager<MapMarker>,
) : DefaultClusterRenderer<MapMarker>(context, map, clusterManager) {

private val mapMarkerView: MapMarkerView = MapMarkerView(context)
private val markerIconGenerator = IconGenerator(context)

init {
markerIconGenerator.setBackground(null)
markerIconGenerator.setContentView(mapMarkerView)
}

override fun onBeforeClusterItemRendered(clusterItem: MapMarker, markerOptions: MarkerOptions) {
val data = getItemIcon(clusterItem)
markerOptions
.icon(data.bitmapDescriptor)
.anchor(data.anchorU, data.anchorV)
}

override fun onClusterItemUpdated(clusterItem: MapMarker, marker: Marker) {
val data = getItemIcon(clusterItem)
marker.setIcon(data.bitmapDescriptor)
marker.setAnchor(data.anchorU, data.anchorV)
}

override fun onBeforeClusterRendered(
cluster: Cluster<MapMarker>,
markerOptions: MarkerOptions
) {
val data = getClusterIcon(cluster)
markerOptions
.icon(data.bitmapDescriptor)
.anchor(data.anchorU, data.anchorV)
}

override fun onClusterUpdated(cluster: Cluster<MapMarker>, marker: Marker) {
val data = getClusterIcon(cluster)
marker.setIcon(data.bitmapDescriptor)
marker.setAnchor(data.anchorU, data.anchorV)
}

override fun shouldRenderAsCluster(cluster: Cluster<MapMarker>): Boolean = cluster.size > 1
}

private fun getItemIcon(marker: MapMarker): IconData {
mapMarkerView.setContent(
circle = MapMarkerView.CircleContent.Marker,
title = marker.titleText
)
val icon: Bitmap = markerIconGenerator.makeIcon()
val middleBalloon = dpToPx(mapMarkerView.context, 24)
return IconData(
bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(icon),
anchorU = middleBalloon / 2 / icon.width,
anchorV = 1f
)
}

private fun getClusterIcon(cluster: Cluster<MapMarker>): IconData {
mapMarkerView.setContent(
circle = MapMarkerView.CircleContent.Cluster(
count = cluster.size
),
title = null
)

val icon: Bitmap = markerIconGenerator.makeIcon()
val middleBalloon = dpToPx(context, 40)
return IconData(
bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(icon),
anchorU = middleBalloon / 2 / icon.width,
anchorV = 1f
)
}

override fun onMapReady(googleMap: GoogleMap) {

...
scope.launch {
(application as MapApplication).dataFlow.combine(boundariesListener.boundariesFlow) { data, boundaries ->
data to boundaries
}.collect { (data, boundaries) ->

val markers = data.filter { boundaries.bounds.includesLocation(it.lat, it.lon) }
.map { marker ->
MapMarker(
titleText = "Item ${marker.hashCode()}",
location = LatLng(marker.lat, marker.lon)
)
}
clusterManager.clearItems()
clusterManager.addItems(markers)
clusterManager.cluster()
}
}
}

Picasso.get()
.load(imageUrl)
.resize(size, size)
.centerCrop()
.into(object : Target {

})

dependencies {

implementation 'com.squareup.picasso:picasso:<VERSION>'

private fun getItemIcon(marker: MapMarker): IconData {
val iconToShow: MapMarker.Icon = when (marker.icon) {
is MapMarker.Icon.UrlIcon -> {
val cachedIcon = loadedImages.get(marker.icon.url)

if (cachedIcon == null) {
loadBitmapImage(marker.icon.url)
}
cachedIcon?.let { MapMarker.Icon.BitmapIcon(marker.icon.url, it) } ?: marker.icon
}

else -> marker.icon
}

private val loadedImages = LruCache<String, Bitmap>(30)
data class MapMarker(
val icon: Icon,
val titleText: String,
@ColorInt val pinColor: Int,
val location: LatLng,
) : ClusterItem {

...

sealed interface Icon {
val url: String
data class Placeholder(override val url: String) : Icon
data class BitmapIcon(override val url: String, val image: Bitmap) : Icon
}
}

fun setContent(
circle: CircleContent,
title: String?,
@ColorInt pinColor: Int,
) {

when (circle) {
is CircleContent.Cluster -> {
...
}

is CircleContent.Marker -> {
binding.mapMarkerViewClusterText.isVisible = false
val icon = circle.mapMarkerIcon
val drawable = getIconDrawable(markerIcon = icon)
binding.mapMarkerViewIcon.setImageDrawable(drawable)

...

private fun getIconDrawable(
markerIcon: MapMarker.Icon,
): Drawable? {

val drawable = when (markerIcon) {
is MapMarker.Icon.BitmapIcon -> {
RoundedBitmapDrawableFactory.create(resources, markerIcon.image).apply {
isCircular = true
cornerRadius = max(markerIcon.image.width, markerIcon.image.height) / 2.0f
}
}

is MapMarker.Icon.Placeholder -> {
// Here we are just waiting for image to be loaded
null
}
}
return drawable
}

private fun loadBitmapImage(imageUrl: String) {
val size = dpToPx(context, 40).toInt()
val target = IconTarget(imageUrl)

Picasso.get()
.load(BuildConfig.IMAGES_URL + imageUrl)
.resize(size, size)
.centerCrop()
.into(target)
}

inner class IconTarget(private val imageUrl: String) : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
loadedImages.put(imageUrl, bitmap)
callback.onImageLoaded(icon = MapMarker.Icon.BitmapIcon(imageUrl, bitmap))
}

override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {}

override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
}

interface Callback {
fun onImageLoaded(icon: MapMarker.Icon.BitmapIcon)
}

John Wick: Chapter 4 (FREE) FULLMOVIE The Super Mario Bros Movie avatar 2 Where To Watch Creed 3 Free At Home Knock at the Cabin (2023) FullMovie Where To Watch Ant-Man 3 and the Wasp: Quantumania Cocaine Bear 2023 (FullMovie) Scream 6 Full Movie
Updated: April 25, 2023 — 10:16 pm

Leave a Reply

Your email address will not be published. Required fields are marked *

androidkaki.com © 2023 Android kaki