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
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.
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>
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)
}
}
}
}
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.
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.
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?