{"id":382657,"date":"2024-06-29T04:24:35","date_gmt":"2024-06-29T04:24:35","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=382657"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2024-06-29T04:24:35","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=382657","title":{"rendered":"<span>Android MapView with clustering and dynamic images in markers<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1f4\/faf\/4a8\/1f4faf4a80f2e0895466d439c1bf8b6c.png\" width=\"1400\" height=\"700\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1f4\/faf\/4a8\/1f4faf4a80f2e0895466d439c1bf8b6c.png\"\/><\/figure>\n<p>In this article, I will cover basic work with MapView (and MapFragment) markers, learn how to implement marker clustering, i.e. group markers located close to each other, and display dynamic icons in the markers, i.e. load by url and display remote images. Ultimately, we will create such map with clusters and dynamic image loading (in background).<\/p>\n<h2>Basic markers loading and displaying<\/h2>\n<p>Create a new project in Android Studio and add an Activity with\u00a0<em>Maps Activity<\/em>template.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/2bc\/d1d\/68f\/2bcd1d68f16c3682ee739b2b436f311a.png\" width=\"1400\" height=\"700\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/2bc\/d1d\/68f\/2bcd1d68f16c3682ee739b2b436f311a.png\"\/><\/figure>\n<p>In the newly created project, open AndroidManifest.xml. There, you will see a comment requesting to get an API_KEY by the\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/get-api-key\" rel=\"noopener noreferrer nofollow\"><u>link<\/u><\/a>\u00a0and add it to the metadata.<\/p>\n<pre><code>&lt;meta-data     android:name=\"com.google.android.geo.API_KEY\"     android:value=\"YOUR_API_KEY\" \/><\/code><\/pre>\n<p>If everything is correct, you will be able to launch the app and see a map with a marker somewhere in Australia. This is a very basic app and in case of any problems, please refer to the documentation or StackOverflow.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e67\/1f1\/371\/e671f1371eaecde8d38716bc3d379db2.png\" width=\"1400\" height=\"700\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e67\/1f1\/371\/e671f1371eaecde8d38716bc3d379db2.png\"\/><\/figure>\n<p>Next, add dynamic marker upload. In our example, we will use a JSON file with the marker locations and download them using Retrofit.<\/p>\n<pre><code>[   {     \"lat\": 59.92140394439577,     \"lon\": 30.445576954709395,     \"icon\": \"1619152.jpg\"   },   {     \"lat\": 59.93547541514004,     \"lon\": 30.21481515274267,     \"icon\": \"1712315710.jpg\"   },<\/code><\/pre>\n<p>Now we are interested only in location but we will use the\u00a0<code>icon<\/code>\u00a0field in the next sections of this article.<\/p>\n<p>Add\u00a0<code>MapApplication<\/code>\u00a0class to our app. It will be responsible for data loading in the app. Of course in the real app we would use a separate class and modules for such business logic and follow the Clean Architecture principles but this is not the focus of this article.<\/p>\n<pre><code>class MapApplication : Application() {      private val context = CoroutineScope(Dispatchers.Default)        private val _dataFlow = MutableSharedFlow&lt;List&lt;MarkerData>>(         replay = 1,         onBufferOverflow = BufferOverflow.DROP_OLDEST     )        val dataFlow: Flow&lt;List&lt;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())         }     } }<\/code><\/pre>\n<p><code>MarkerLocationsService<\/code>\u00a0is a Retrofit service that downloads our markers.<\/p>\n<p>Update\u00a0<code>onMapReady<\/code>\u00a0code in\u00a0<code>MapActivity<\/code>\u00a0so that it gets this list:<\/p>\n<pre><code>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)))             }         }     } }<\/code><\/pre>\n<p>Great! Now we know how to display markers received from the server. But there may be a lot of markers, so let\u2019s add the option of adding markers to the map taking into account the region which is currently displayed. For this, we need\u00a0<code>OnCameraIdleListener<\/code>\u00a0that shows that the user has interacted with the map (changed the scale or moved to another location), which means that we need to recalculate map boundaries and display markers for it only.<\/p>\n<pre><code>internal class BoundariesListener(     private val map: GoogleMap, ) : GoogleMap.OnCameraIdleListener {      private val _boundariesFlow = MutableSharedFlow&lt;LatLngBounds>(         replay = 1,         onBufferOverflow = BufferOverflow.DROP_OLDEST,     )     val boundariesFlow: Flow&lt;LatLngBounds> = _boundariesFlow      override fun onCameraIdle() {         val boundaries = map.projection.visibleRegion.latLngBounds         _boundariesFlow.tryEmit(boundaries)     } }<\/code><\/pre>\n<p>Now, we can assign\u00a0<code>BoundariesListener<\/code>\u00a0to the map object, and, when the map boundaries gets changed, we will filter the markers taking into account currently shown area:<\/p>\n<pre><code>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 &amp;&amp; this.southwest.latitude &lt; lat &amp;&amp;             this.northeast.longitude > lon &amp;&amp; this.southwest.longitude &lt; lon  }<\/code><\/pre>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/08c\/dbf\/b66\/08cdbfb66b1b34506475ac2215722d78.png\" width=\"1400\" height=\"700\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/08c\/dbf\/b66\/08cdbfb66b1b34506475ac2215722d78.png\"\/><\/figure>\n<p>Excellent! After this step, we can display markers received from the server, on the map; however, if there are many items positioned close to each other, the markers will overlap, so let\u2019s see how we can group them.<\/p>\n<h2>Marker clustering<\/h2>\n<p>The screenshot above shows a large number of markers that can overlap and, in a larger scale, merge into a single point. That\u2019s why Google\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/utility\/marker-clustering\" rel=\"noopener noreferrer nofollow\"><u>created a library<\/u><\/a>\u00a0that can group markers if they are close to each other. In this case, instead of several markers, we display one with general information. Let\u2019s try to implement this solution in our app.<\/p>\n<p>To add the clustering feature, add a dependency on\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/utility\" rel=\"noopener noreferrer nofollow\"><u>Maps SDK for Android Utility<\/u><\/a>\u00a0to the project.<\/p>\n<pre><code>dependencies {    ...      implementation 'com.google.maps.android:android-maps-utils:&lt;version>' }<\/code><\/pre>\n<p>First, we need\u00a0<a href=\"https:\/\/github.com\/googlemaps\/android-maps-utils\/blob\/master\/library\/src\/main\/java\/com\/google\/maps\/android\/clustering\/ClusterManager.java\" rel=\"noopener noreferrer nofollow\"><u>ClusterManager<\/u><\/a>\u00a0that will get big part of the job done. It processes a list of the markers, decides which of them are close to each other and, therefore, should be replaced with one group marker (cluster). The second important component is\u00a0<code>ClusterRenderer<\/code>\u00a0that, as it goes from its name, draws markers and cluster items. Also, we have a base class\u00a0<code>DefaultClusterRenderer<\/code>\u00a0that implements quite a lot of the basic logic, so we can inherit from it and care only about important things. Both these classes work with\u00a0<code>ClusterItem<\/code>\u00a0which stores the marker information such as location. It is an interface, so let\u2019s, first of all, create its implementation:<\/p>\n<pre><code>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 }<\/code><\/pre>\n<p>It\u2019s plain and simple: we store information about the location and some marker label. It actually can contain more information about the marker, but we will look at it later. Then, create an instance of\u00a0<code>ClusterManager<\/code>\u00a0in\u00a0<code>onMapReady<\/code>:<\/p>\n<pre><code>override fun onMapReady(googleMap: GoogleMap) {     mMap = googleMap      val boundariesListener = BoundariesListener(googleMap)      val clusterManager = ClusterManager&lt;MapMarker>(this, mMap)     val mapRenderer = MapMarkersRenderer(         context = this,         callback = this,         map = mMap,         clusterManager = clusterManager     )     clusterManager.renderer = mapRenderer<\/code><\/pre>\n<p>Here, you can see\u00a0<code>MapMarkersRenderer<\/code>\u00a0is used, so add it to the project, too:<\/p>\n<pre><code>class MapMarkersRenderer(     context: Context,     map: GoogleMap,     clusterManager: ClusterManager&lt;MapMarker>, ) : DefaultClusterRenderer&lt;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&lt;MapMarker>,         markerOptions: MarkerOptions     ) {         val data = getClusterIcon(cluster)         markerOptions             .icon(data.bitmapDescriptor)             .anchor(data.anchorU, data.anchorV)     }      override fun onClusterUpdated(cluster: Cluster&lt;MapMarker>, marker: Marker) {         val data = getClusterIcon(cluster)         marker.setIcon(data.bitmapDescriptor)         marker.setAnchor(data.anchorU, data.anchorV)     }      override fun shouldRenderAsCluster(cluster: Cluster&lt;MapMarker>): Boolean = cluster.size > 1 }<\/code><\/pre>\n<p>In this code snippet we have two pairs of methods\u00a0<code>onBefore\u2026Rendered<\/code>\u00a0and\u00a0<code>on...Updated<\/code>. They are called when a cluster or cluster item (separate marker) is created or updated (when the marker already exists on the map and we just want to update it\u2019s presentation, for example color, text or something else). Also, we have a method\u00a0<code>shouldRenderAsCluster<\/code>, where we define whether we need to display a group of markers as a cluster or as separate markers. In the code above, we want to show the cluster instead of separate markers if the group contains more than one element.<\/p>\n<p><code>ClusterRenderer<\/code>\u00a0makes it possible to draw very custom clusters and markers so we will use this option and create a custom view\u00a0<code>MapMarkerView<\/code>\u00a0and draw it in methods\u00a0<code>getClusterIcon()<\/code>\u00a0and\u00a0<code>getItemIcon()<\/code>. I\u2019m not going to provide code for\u00a0<code>MapMarkerView<\/code>\u00a0since it\u2019s a quite simple android\u00a0<code>View<\/code>\u00a0that shows image and text only. But you can check it out in\u00a0<a href=\"https:\/\/github.com\/rchugunov\/android_map_dynamic_pins\/blob\/main\/app\/src\/main\/java\/com\/rchugunov\/dynamicpinsview\/map\/MapMarkerView.kt\" rel=\"noopener noreferrer nofollow\"><u>my project on Github<\/u><\/a>. What we need is to send some content to this View and then draw its state, receive bitmap with\u00a0<code>markerIconGenerator.makeIcon<\/code>\u00a0and send it to\u00a0<code>marker.setIcon<\/code>, so methods getClusterIcon and getItemIcon will look like this:<\/p>\n<pre><code>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&lt;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     ) }<\/code><\/pre>\n<p>Now, we can render clusters and markers. The last thing to do is to send the list of markers directly to the\u00a0<code>ClusterManager<\/code>\u00a0so that it determined which of them and in which way to display. Please note that it is crucial to call\u00a0<code>clusterManager.cluster()<\/code>\u00a0after you modified the collection of items.<\/p>\n<pre><code>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()         }     } }<\/code><\/pre>\n<p>If we now launch our app, we will see the map with clusters and markers. They are displayed according to the logic that we added to\u00a0<code>MapMarkerView<\/code>\u00a0and\u00a0<code>MapMarkersRenderer<\/code>, i.e. it can display static icons (the purple little human) and text only.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/467\/cec\/cef\/467ceccef1a031fa9732790c8e85607c.png\" width=\"1400\" height=\"700\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/467\/cec\/cef\/467ceccef1a031fa9732790c8e85607c.png\"\/><\/figure>\n<p>Next, we are going to learn how to upload dynamic images stored remotely on the server and how to render them in markers.<\/p>\n<h2>Dynamic images in the markers<\/h2>\n<p>If you open Google Maps app, you will see that 99% of the icons are static resources matching the category of a place: restaurant, hotel, museum, etc. Google Maps can also display icons of selected locations but their number relatively to the other icons is very low.<\/p>\n<p>I believe this restriction aims two objectives. The first one is to ensure optimal performance. Obviously, image loading from the Internet requires additional resources for the download and drawing, as compared to static icons. The second reason is to avoid visual littering of markers in the map since multiple different icons create visual mess and make objects under the layer of markers less distinct.<\/p>\n<p>Unfortunately,\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/utility\" rel=\"noopener noreferrer nofollow\"><strong><u>Maps SDK for Android Utility Library<\/u><\/strong><\/a>\u00a0doesn\u2019t support loading and showing dynamic images out of the box. Normally we load images in the background thread, and loaded result will be passed in a callback and placed into ImageView in the main thread (i.e. in\u00a0<a href=\"https:\/\/square.github.io\/picasso\/\" rel=\"noopener noreferrer nofollow\"><u>Picasso<\/u><\/a>).<\/p>\n<pre><code>Picasso.get()     .load(imageUrl)     .resize(size, size)     .centerCrop()     .into(object : Target {      })<\/code><\/pre>\n<p>In theory, we could load images in methods\u00a0<code>onBefore\u2026Rendered<\/code>\u00a0and\u00a0<code>on...Updated<\/code>, but they are synchronous, i.e. the drawing must occur directly in them and block the UI thread.<\/p>\n<p>However, we can overcome this by first rendering a placeholder and starting the download of the image simultaneously. And once it is loaded, update the markers with the loaded and cached image. Here is a diagram for this:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/86e\/b07\/0c6\/86eb070c60da34db1f587739e526bdca.jpeg\" width=\"1400\" height=\"935\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/86e\/b07\/0c6\/86eb070c60da34db1f587739e526bdca.jpeg\" data-blurred=\"true\"\/><\/figure>\n<p>We will use Picasso library for the image upload. For this, add it to the project:<\/p>\n<pre><code>dependencies {      implementation 'com.squareup.picasso:picasso:&lt;VERSION>'<\/code><\/pre>\n<p>Now, update\u00a0<code>MapMarkersRenderer<\/code>, adding it the ability of loading the icon. We want to display images in separate markers only, so update only method\u00a0<code>getItemIcon<\/code>, used in\u00a0<code>onClusterItemUpdated<\/code>\u00a0and\u00a0<code>onBeforeClusterItemRendered<\/code><\/p>\n<pre><code>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     }<\/code><\/pre>\n<p>Here\u00a0<code>loadedImages<\/code>\u00a0is a cache of previously loaded icons. LruCache will be an excellent choice for this.<\/p>\n<pre><code>private val loadedImages = LruCache&lt;String, Bitmap>(30)<\/code><\/pre>\n<p>Next, add a container for our icons so that they could be stored in\u00a0<code>MapMarker<\/code>and drawn in a\u00a0<code>MapMarkerView<\/code>.<\/p>\n<pre><code>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     } }<\/code><\/pre>\n<p>Let\u2019s now also update the\u00a0<code>MapMarkerView<\/code>\u00a0code for it be able to draw our icons:<\/p>\n<pre><code>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)             ...<\/code><\/pre>\n<pre><code>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 }<\/code><\/pre>\n<p>Back to\u00a0<code>MapMarkersRenderer<\/code>\u00a0we need to define method\u00a0<code>loadBitmapImage<\/code>. We pass the image ID to it and download the image using Picasso. The result will be received in\u00a0<code>ImageTarget<\/code>\u00a0. Here, we cache the image and return in the\u00a0<code>Callback<\/code>\u00a0to later update\u00a0<code>ClusterManager<\/code>\u00a0with our uploaded image.<\/p>\n<pre><code>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) }<\/code><\/pre>\n<p>In this code snipper, for simplicity we don\u2019t handle any errors in\u00a0<code>onBitmapFailed<\/code>\u00a0and don\u2019t check if an image with such URL is already downloaded (we have it in our cache), but I recommend adding such a check. You can find an example of such code\u00a0<a href=\"https:\/\/github.com\/rchugunov\/android_map_dynamic_pins\/blob\/main\/app\/src\/main\/java\/com\/rchugunov\/dynamicpinsview\/map\/MapMarkersRenderer.kt#L163\" rel=\"noopener noreferrer nofollow\"><u>here<\/u><\/a>. Next, when we call\u00a0<code>callback.onImageLoaded<\/code>, we assume that our activity will handle this event, find the marker to be updated, and tell\u00a0<code>ClusterManager<\/code>\u00a0to update it on the map. After that,\u00a0<code>ClusterManager<\/code>\u00a0will see that the image just uploaded already exists in the cache and, therefore, can be sent to\u00a0<code>MapMarkerView<\/code>, and the view can be rendered with the image inside. If everything is correct, we will have the map with clusters and markers that can display the images uploaded asynchronously, when we launch the app.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/596\/39d\/bcc\/59639dbccc7cdd5d5214f2ee81a6a319.png\" width=\"1400\" height=\"700\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/596\/39d\/bcc\/59639dbccc7cdd5d5214f2ee81a6a319.png\"\/><\/figure>\n<p>You can find the full code of this app on my\u00a0<a href=\"https:\/\/github.com\/rchugunov\/android_map_dynamic_pins\" rel=\"noopener noreferrer nofollow\"><u>Github<\/u><\/a>. I\u2019ve added certain optimizations that can be of use when you implement such feature in a real-world project. In addition, I would recommend to use a\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/styling\" rel=\"noopener noreferrer nofollow\"><u>custom style for your map<\/u><\/a>. This will help to remove some elements displayed on the map by default, such as businesses (restaurants, hotels, etc.), to focus user\u2019s attention on the markers and clusters.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/743746\/\"> https:\/\/habr.com\/ru\/articles\/743746\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><\/figure>\n<p>In this article, I will cover basic work with MapView (and MapFragment) markers, learn how to implement marker clustering, i.e. group markers located close to each other, and display dynamic icons in the markers, i.e. load by url and display remote images. Ultimately, we will create such map with clusters and dynamic image loading (in background).<\/p>\n<h2>Basic markers loading and displaying<\/h2>\n<p>Create a new project in Android Studio and add an Activity with\u00a0<em>Maps Activity<\/em>template.<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>In the newly created project, open AndroidManifest.xml. There, you will see a comment requesting to get an API_KEY by the\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/get-api-key\" rel=\"noopener noreferrer nofollow\"><u>link<\/u><\/a>\u00a0and add it to the metadata.<\/p>\n<pre><code>&lt;meta-data     android:name=\"com.google.android.geo.API_KEY\"     android:value=\"YOUR_API_KEY\" \/><\/code><\/pre>\n<p>If everything is correct, you will be able to launch the app and see a map with a marker somewhere in Australia. This is a very basic app and in case of any problems, please refer to the documentation or StackOverflow.<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>Next, add dynamic marker upload. In our example, we will use a JSON file with the marker locations and download them using Retrofit.<\/p>\n<pre><code>[   {     \"lat\": 59.92140394439577,     \"lon\": 30.445576954709395,     \"icon\": \"1619152.jpg\"   },   {     \"lat\": 59.93547541514004,     \"lon\": 30.21481515274267,     \"icon\": \"1712315710.jpg\"   },<\/code><\/pre>\n<p>Now we are interested only in location but we will use the\u00a0<code>icon<\/code>\u00a0field in the next sections of this article.<\/p>\n<p>Add\u00a0<code>MapApplication<\/code>\u00a0class to our app. It will be responsible for data loading in the app. Of course in the real app we would use a separate class and modules for such business logic and follow the Clean Architecture principles but this is not the focus of this article.<\/p>\n<pre><code>class MapApplication : Application() {      private val context = CoroutineScope(Dispatchers.Default)        private val _dataFlow = MutableSharedFlow&lt;List&lt;MarkerData>>(         replay = 1,         onBufferOverflow = BufferOverflow.DROP_OLDEST     )        val dataFlow: Flow&lt;List&lt;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())         }     } }<\/code><\/pre>\n<p><code>MarkerLocationsService<\/code>\u00a0is a Retrofit service that downloads our markers.<\/p>\n<p>Update\u00a0<code>onMapReady<\/code>\u00a0code in\u00a0<code>MapActivity<\/code>\u00a0so that it gets this list:<\/p>\n<pre><code>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)))             }         }     } }<\/code><\/pre>\n<p>Great! Now we know how to display markers received from the server. But there may be a lot of markers, so let\u2019s add the option of adding markers to the map taking into account the region which is currently displayed. For this, we need\u00a0<code>OnCameraIdleListener<\/code>\u00a0that shows that the user has interacted with the map (changed the scale or moved to another location), which means that we need to recalculate map boundaries and display markers for it only.<\/p>\n<pre><code>internal class BoundariesListener(     private val map: GoogleMap, ) : GoogleMap.OnCameraIdleListener {      private val _boundariesFlow = MutableSharedFlow&lt;LatLngBounds>(         replay = 1,         onBufferOverflow = BufferOverflow.DROP_OLDEST,     )     val boundariesFlow: Flow&lt;LatLngBounds> = _boundariesFlow      override fun onCameraIdle() {         val boundaries = map.projection.visibleRegion.latLngBounds         _boundariesFlow.tryEmit(boundaries)     } }<\/code><\/pre>\n<p>Now, we can assign\u00a0<code>BoundariesListener<\/code>\u00a0to the map object, and, when the map boundaries gets changed, we will filter the markers taking into account currently shown area:<\/p>\n<pre><code>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 &amp;&amp; this.southwest.latitude &lt; lat &amp;&amp;             this.northeast.longitude > lon &amp;&amp; this.southwest.longitude &lt; lon  }<\/code><\/pre>\n<figure class=\"full-width\"><\/figure>\n<p>Excellent! After this step, we can display markers received from the server, on the map; however, if there are many items positioned close to each other, the markers will overlap, so let\u2019s see how we can group them.<\/p>\n<h2>Marker clustering<\/h2>\n<p>The screenshot above shows a large number of markers that can overlap and, in a larger scale, merge into a single point. That\u2019s why Google\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/utility\/marker-clustering\" rel=\"noopener noreferrer nofollow\"><u>created a library<\/u><\/a>\u00a0that can group markers if they are close to each other. In this case, instead of several markers, we display one with general information. Let\u2019s try to implement this solution in our app.<\/p>\n<p>To add the clustering feature, add a dependency on\u00a0<a href=\"https:\/\/developers.google.com\/maps\/documentation\/android-sdk\/utility\" rel=\"noopener noreferrer nofollow\"><u>Maps SDK for Android Utility<\/u><\/a>\u00a0to the project.<\/p>\n<pre><code>dependencies {    ...      implementation 'com.google.maps.android:android-maps-utils:&lt;version>' }<\/code><\/pre>\n<p>First, we need\u00a0<a href=\"https:\/\/github.com\/googlemaps\/android-maps-utils\/blob\/master\/library\/src\/main\/java\/com\/google\/maps\/android\/clustering\/ClusterManager.java\" rel=\"noopener noreferrer nofollow\"><u>ClusterManager<\/u><\/a>\u00a0that will get big part of the job done. It processes a list of the markers, decides which of them are close to each other and, therefore, should be replaced with one group marker (cluster). The second important component is\u00a0<code>ClusterRenderer<\/code>\u00a0that, as it goes from its name, draws markers and cluster items. Also, we have a base class\u00a0<code>DefaultClusterRenderer<\/code>\u00a0that implements quite a lot of the basic logic, so we can inherit from it and care only about important things. Both these classes work with\u00a0<code>ClusterItem<\/code>\u00a0which stores the marker information such as location. It is an interface, so let\u2019s, first of all, create its implementation:<\/p>\n<pre><code>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 }<\/code><\/pre>\n<p>It\u2019s plain and simple: we store information about the location and some marker label. It actually can contain more information about the marker, but we will look at it later. Then, create an instance of\u00a0<code>ClusterManager<\/code>\u00a0in\u00a0<code>onMapReady<\/code>:<\/p>\n<pre><code>override fun onMapReady(googleMap: GoogleMap) {     mMap = googleMap      val boundariesListener = BoundariesListener(googleMap)      val clusterManager = ClusterManager&lt;MapMarker>(this, mMap)     val mapRenderer = MapMarkersRenderer(         context = this,         callback = this,         map = mMap,         clusterManager = clusterManager     )     clusterManager.renderer = mapRenderer<\/code><\/pre>\n<p>Here, you can see\u00a0<code>MapMarkersRenderer<\/code>\u00a0is used, so add it to the project, too:<\/p>\n<pre><code>class MapMarkersRenderer(     context: Context,     map: GoogleMap,     clusterManager: ClusterManager&lt;MapMarker>, ) : DefaultClusterRenderer&lt;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&lt;MapMarker>,         markerOptions: MarkerOptions     ) {         val data = getClusterIcon(cluster)         markerOptions             .icon(data.bitmapDescriptor)             .anchor(data.anchorU, data.anchorV)     }      override fun onClusterUpdated(cluster: Cluster&lt;MapMarker>, marker: Marker) {         val data = getClusterIcon(cluster)         marker.setIcon(data.bitmapDescriptor)         marker.setAnchor(data.anchorU, data.anchorV)     }      override fun shouldRenderAsCluster(cluster: Cluster&lt;MapMarker>): Boolean = cluster.size > 1 }<\/code><\/pre>\n<p>In this code snippet we have two pairs of methods\u00a0<code>onBefore\u2026Rendered<\/code>\u00a0and\u00a0<code>on...Updated<\/code>. They are called when a cluster or cluster item (separate marker) is created or updated (when the marker already exists on the map and we just want to update it\u2019s presentation, for example color, text or something else). Also, we have a method\u00a0<code>shouldRenderAsCluster<\/code>, where we define whether we need to display a group of markers as a cluster or as separate markers. In the code above, we want to show the cluster instead of separate markers if the group contains more than one element.<\/p>\n<p><code>ClusterRenderer<\/code>\u00a0makes it possible to draw very custom clusters and markers so we will use this option and create a custom view\u00a0<code>MapMarkerView<\/code>\u00a0and draw it in methods\u00a0<code>getClusterIcon()<\/code>\u00a0and\u00a0<code>getItemIcon()<\/code>. I\u2019m not going to provide code for\u00a0<code>MapMarkerView<\/code>\u00a0since it\u2019s a quite simple android\u00a0<code>View<\/code>\u00a0that shows image and text only. But you can check it out in\u00a0<a href=\"https:\/\/github.com\/rchugunov\/android_map_dynamic_pins\/blob\/main\/app\/src\/main\/java\/com\/rchugunov\/dynamicpinsview\/map\/MapMarkerView.kt\" rel=\"noopener noreferrer nofollow\"><u>my project on Github<\/u><\/a>. What we need is to send some content to this View and then draw its state, receive bitmap with\u00a0<code>markerIconGenerator.makeIcon<\/code>\u00a0and send it to\u00a0<code>marker.setIcon<\/code>, so methods getClusterIcon and getItemIcon will look like this:<\/p>\n<pre><code>private fun getItemIcon(marker: MapMarker): IconData <\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-382657","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/382657","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=382657"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/382657\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=382657"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=382657"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=382657"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}