We say a
lot about...
everything

Creating Custom Markers for Google Maps – for Android Development
development

Creating Custom Markers for Google Maps – for Android Development

Popular Articles

Creating Custom Markers for Google Maps – for Android Development

A simple guide to creating custom markers for the Google Maps API

Published on June 6, 2024

by

Ars FuturaArs Futura

Filed under development

Many Android mobile apps leverage the Google Maps API, which offers plenty of features that can be customized to meet specific needs. From adjusting map styles and colors to tailoring roads and labels, developers have extensive control over the visual elements. While these default customization options are often sufficient, there are scenarios where the out-of-the-box features fall short.

So, when dealing with mobile app map markers, we can use the default location markers provided by Google and tailor their attributes, such as color, opacity, and associated data. Alternatively, we can substitute the default marker image with a personalized one using the BitmapDescriptorFactory class. In this blog, we’ll explore how that could be achieved, and if you’d like to know how to do it yourself, keep on reading!

Feel free to check out the Github repo with the source code.

Setting up Google Maps 

Let's begin by presenting a scenario where you’d need such customization.

Imagine we're developing a delivery app that involves various markers based on specific data for each location. 

The four distinct markers we'll need are:

         Depot: Marks the depot, serving as both the initial and final stop.

         Unfinished Task: Indicates a location the driver must visit, featuring a sequence number in the center to denote the delivery order. Its color varies based on the delivery status: green for on-time, yellow for potential delay, and red if the delivery is at risk of being late.

         Finished Task: Similar to the unfinished task, but with a checkmark replacing the sequence number.

         Restock: Marks a location where the driver can restock its inventory.

In this post, we'll explore how to implement such a customization to enhance the functionality of our delivery app.

We'll start by implementing Google Maps into our project. To achieve this we'll need to log in to Google Developer Console, create a new project, and generate an API key.

This API key should be added to our AndroidManifest.xml file like so:

<application>

...

   <meta-data

       android:name="com.google.android.geo.API_KEY"

       android:value="${MAPS_API_KEY}" />

...

</application>

We'll add the Google Maps dependency into our module (app-level) build.gradle.kts file along with Kotlin extensions for the Google Maps SDK.

implementation("com.google.maps.android:maps-ktx:5.0.0")

implementation("com.google.android.gms:play-services-maps:18.2.0") 

Now we're ready to add the map to our Activity.

The Activity layout will have a Google Maps fragment. Since data binding will be used throughout this project, everything will be wrapped up inside the <layout> tag.

 

<?xml version="1.0" encoding="utf-8"?>


<layout xmlns:android="http://schemas.android.com/apk/res/android"

   xmlns:tools="http://schemas.android.com/tools">



   <androidx.fragment.app.FragmentContainerView

       android:id="@+id/mapFragment"

       android:name="com.google.android.gms.maps.SupportMapFragment"

       android:layout_width="match_parent"

       android:layout_height="match_parent"

       tools:context=".MapsActivity" />


</layout>

Assuming all the configurations are accurate, launching the app at this point will display the map.

The next step is to create a map marker object. This will contain all the information we need to create our map markers.

data class MapMarkerObject(
	val sequenceId: Int,
	val completed: Boolean = false,
	val depot: Boolean = false,
	private val delay: Delay = Delay.ON_TIME,
	val category: Category = Category.MAP_MARKER,
	val locationName: String = "",
	val coordinates: LatLng
) {

	val outerRing: Int
    	get() = if (shouldShowIcon()) color else R.color.white

	val innerRing: Int
    	get() = if (shouldShowIcon()) R.color.white else color

	val drawable: Int
    	get() = if (depot) R.drawable.ic_depot else when (category) {
        	Category.RESTOCK -> R.drawable.ic_refresh
        	else -> R.drawable.ic_check
    	}

	fun shouldShowIcon() = completed || depot || category == Category.RESTOCK

	val color: Int
    	get() = if (depot) R.color.depot else when (delay) {
        	Delay.DELAY_AT_RISK -> R.color.at_risk
        	Delay.DELAY_LATE -> R.color.delayed
        	else -> R.color.on_time
    	}
}

enum class Category {
	MAP_MARKER, PARKING, RESTOCK
}

enum class Delay {
	ON_TIME, DELAY_AT_RISK, DELAY_LATE

The project will include the following color palette:

<color name="black">#FF000000</color>

<color name="white">#FFFFFFFF</color>



<color name="on_time">#52BFAB</color>

<color name="at_risk">#FFC775</color>

<color name="delayed">#F35627</color>


<color name="depot">#83A3A3A3</color> 

We're almost ready to start adding our map markers! But before that, we'll need to create an XML layout for these markers.

<layout xmlns:android="http://schemas.android.com/apk/res/android">

	<data>
    	<import type="androidx.core.content.ContextCompat"/>

    	<variable
        	name="mapMarker"
        	type="com.example.customgooglemapsmarkers.MapMarkerObject" />
	</data>

	<FrameLayout
    	android:id="@+id/custom_marker_view"
    	android:layout_width="40dp"
    	android:layout_height="40dp"
    	android:backgroundTint="@{ContextCompat.getColor(context, mapMarker.outerRing)}"
    	android:background="@drawable/map_circle">

    	<LinearLayout
        	android:id="@+id/linear_layout"
        	android:layout_width="match_parent"
        	android:layout_height="match_parent"
        	android:layout_margin="2dp"
        	android:backgroundTint="@{ContextCompat.getColor(context, mapMarker.innerRing)}"
        	android:background="@drawable/map_circle"
        	android:gravity="center"
        	android:orientation="vertical">

        	<androidx.appcompat.widget.AppCompatImageView
            	android:id="@+id/marker_check"
            	android:layout_width="match_parent"
            	android:layout_height="match_parent"
            	android:layout_gravity="center"
            	android:padding="4dp"
            	android:src="@{ContextCompat.getDrawable(context, mapMarker.drawable)}"
            	android:tint="@{ContextCompat.getColor(context, mapMarker.color)}"
            	app:is_visible="@{mapMarker.shouldShowIcon()}" />

        	<TextView
            	android:id="@+id/stop_text"
            	android:layout_width="match_parent"
            	android:layout_height="wrap_content"
            	android:layout_gravity="center"
            	android:text="@{String.valueOf(mapMarker.sequenceId)}"
            	android:textAlignment="center"
            	android:textColor="@color/white"
            	android:textSize="16sp" 
            	app:is_visible="@{!mapMarker.shouldShowIcon()}" />
    	</LinearLayout>
	</FrameLayout>
</layout>

Adding the Google Maps markers

Now that we're all set up, we can start adding our markers.

Since we have added the KTX library to our project, we're gonna take advantage of several Kotlin features.

To access a GoogleMap, we can simply copy and paste the code snippet provided in the documentation:

lifecycleScope.launch {

   lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {

       val mapFragment: SupportMapFragment? =

           supportFragmentManager.findFragmentById(R.id.map) as? SupportMapFragment

       val googleMap: GoogleMap? = mapFragment?.awaitMap()

   }

}

Next, we're gonna create some dummy data for our markers:

 

private fun getMapMarkers(): List<MapMarkerObject> {

   val marker1 = MapMarkerObject(

       1,

       locationName = "Arena Centar", coordinates = LatLng(45.77, 15.93)

   )

   val marker2 = MapMarkerObject(

       2, completed = true,

       category = Category.RESTOCK,

       locationName = "Avenue Mall", coordinates = LatLng(45.77, 15.97)

   )

   val marker3 = MapMarkerObject(

       0, depot = true,

       locationName = "Ars Futura", coordinates = LatLng(45.79, 15.95)

   )

   val marker4 = MapMarkerObject(

       3,

       delay = Delay.DELAY_AT_RISK,

       locationName = "Supernova Garden Mall", coordinates = LatLng(45.83, 16.04)

   )

   val marker5 = MapMarkerObject(

       4,

       delay = Delay.DELAY_LATE,

       locationName = "City Center one West", coordinates = LatLng(45.79, 15.88)

   )

   val marker6 = MapMarkerObject(

       5, completed = true,

       locationName = "City Center one East", coordinates = LatLng(45.80, 16.05)

   )

   val marker7 = MapMarkerObject(

       6, completed = true,

       delay = Delay.DELAY_LATE,

       locationName = "Westgate Shopping City", coordinates = LatLng(45.87, 15.82)

   )

   return listOf(marker1, marker2, marker3, marker4, marker5, marker6, marker7)

}

Adding a custom map marker can be done using the addMarker() method:

 

lifecycleScope.launch {

   lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {

       val mapFragment: SupportMapFragment? =

       supportFragmentManager.findFragmentById(R.id.mapFragment) as? SupportMapFragment

       googleMap = mapFragment?.awaitMap()



       for (mapMarker in getMapMarkers()) {

           googleMap?.addMarker {

               position(mapMarker.coordinates)

               title(mapMarker.locationName)

           }

       }

   }

}

Displaying a custom map marker 

By launching the app now, we can see markers on our map. There are two problems we need to fix next - we're using the default marker icon and those markers are not centered.

Let's start by adding two new methods. The first one will convert the device-independent pixels to pixels, and the second one will be used to convert our XML layout into a bitmap image.

 

fun dpToPx(dp: Int): Int {

   return (dp * Resources.getSystem().displayMetrics.density).toInt()

}




private fun getMarkerBitmapFromView(marker: MapMarkerObject): Bitmap {

   val markerBinding: MapMarkerBinding = DataBindingUtil.inflate(

       layoutInflater

       R.layout.map_marker,

       binding.mapFragment,

       false

   )

   markerBinding.mapMarker = marker

   markerBinding.lifecycleOwner = this

 markerBinding.customMarkerView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)

   markerBinding.customMarkerView.layout(0, 0, dpToPx(MAP_MARKER_SIZE), dpToPx(MAP_MARKER_SIZE))

   markerBinding.linearLayout.layout(

       dpToPx(MAP_MARKER_STROKE), dpToPx(MAP_MARKER_STROKE),

       dpToPx(MAP_MARKER_SIZE - MAP_MARKER_STROKE),

       dpToPx(MAP_MARKER_SIZE - MAP_MARKER_STROKE)

   )



   val returnedBitmap = Bitmap.createBitmap(

       dpToPx(MAP_MARKER_SIZE), dpToPx(MAP_MARKER_SIZE),

       Bitmap.Config.ARGB_8888

   )

   val canvas = Canvas(returnedBitmap)

   canvas.drawColor(Color.WHITE, PorterDuff.Mode.SRC_IN)


   val drawable: Drawable = markerBinding.customMarkerView.background

   drawable.draw(canvas)

   markerBinding.customMarkerView.draw(canvas)

   returnedBitmap.compress(Bitmap.CompressFormat.PNG, 100, ByteArrayOutputStream())

   return returnedBitmap

}

The parameters to the layout are relative to the parent view’s upper left corner. X increases as we move to the right and Y increases as we move down. After we set up our layouts, we need to create a bitmap using the desired height/width. Next, we create a canvas that provides the means to draw on a bitmap, we draw on that canvas, and compress the bitmap into PNG to reduce its size.

We’re all set! Now we can start creating our custom markers. We’ll extract the addMarker method and add an icon using our getMarkerBitmapFromView() method.

 

private fun addAssignmentMarker(mapMarkerObject: MapMarkerObject) {
	googleMap?.addMarker {
    	position(mapMarkerObject.coordinates)
        	icon(BitmapDescriptorFactory.fromBitmap(getMarkerBitmapFromView(mapMarkerObject)))
    	title(mapMarkerObject.locationName)
	}
}

The only problem we have right now is that our marker icons are not centered and we’re zoomed out way too far. We’ll fix that by adding bounds and re-centering the camera.

lifecycleScope.launch {
	lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
    	val mapFragment: SupportMapFragment? =
        supportFragmentManager.findFragmentById(R.id.mapFragment) as? SupportMapFragment
    	googleMap = mapFragment?.awaitMap()

    	val builder = LatLngBounds.Builder()

    	for (mapMarker in getMapMarkers()) {
        	builder.include(mapMarker.coordinates)
        	addAssignmentMarker(mapMarker)
    	}
    	val bounds = builder.build()
    	val cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, 80)

    	googleMap?.moveCamera(cameraUpdate)
	}
}

Centering the camera is done in three steps. First, we need to create a LatLngBounds builder and add all of the marker coordinates into it. Then, we create a CameraUpdate object containing those bounds and some padding. Lastly, move the camera using the moveCamera method and the CameraUpdate object we created earlier.

If all of the steps are completed, your map should have seven custom map markers. Clicking on a custom marker will recenter the map to that marker and display a default info window containing the location name.

Conclusion  

Google Maps offers a versatile toolkit for users to enhance their navigation, exploration, and communication. Throughout this blog, we’ve covered information on how to set up Google Maps and add default and custom Google Maps markers. Whether it’s for business, events, or personal use, understanding how to customize map markers enables us to create more intuitive and efficient mapping experiences.


Related Articles

Join our newsletter

Like what you see? Why not put a ring on it. Or at least your name and e-mail.

Have a project on the horizon?

Let's Talk