Creating a unique and engaging map experience can enhance location-based apps, and adding custom profile markers is a great way to personalize the map. In this post, we’ll guide you through adding a user’s profile picture as a marker on Google Maps using Jetpack Compose — a feature that can be especially useful for visually identifying group members or contacts on a map.
This technique was recently implemented in an open-source app called GroupTrack, a location-sharing app that leverages this feature to make tracking group members easy and intuitive. Let’s explore the setup in detail.
Introduction
Integrating Google Maps into apps significantly enhances user experience by visually distinguishing locations. Custom markers enable developers to create user-friendly applications, allowing users to quickly identify individuals on the map. This is particularly valuable for group or social apps, where profile pictures replace generic markers, simplifying navigation.
In this post, we’ll demonstrate how to implement custom profile markers using Jetpack Compose and the Google Maps Compose library. In our open-source app, GroupTrack, each member’s profile image acts as a unique marker, making it easier for users to locate and differentiate group members in real-time.
Let’s break down the steps needed to implement custom markers like these.
Configure a Map
Requirements
- A Google account with billing
- A Map API key — follow this guide to get your API key
Step 1: Set Up Google Maps in Jetpack Compose
First, add the necessary dependencies in your build.gradle
file and configure your app’s permissions in AndroidManifest.xml
:
implementation("com.google.maps.android:maps-compose:latest-version")
Add your Map API key in the manifest file
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${YOUR_MAPS_API_KEY}" />
Once your setup is complete, initialize the Google Map within your Compose UI.
Add GoogleMap composable
To render a map, use the GoogleMap
composable. Here’s a basic setup, rendering a map centered on Sydney:
@Composable
fun SimpleMap() {
val latLng = LatLng(-33.852334, 151.211245) // Any Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f) // Set zoom level
}
GoogleMap(
modifier = Modifier.fillMaxSize(), // Fill the entire screen
cameraPositionState = cameraPositionState
)
}
Run your app, and you should see a centered map of Sydney.
Step 2: Create a Custom Profile Marker Composable
The profile marker itself is a simple composable that displays a round-cornered profile image, making it versatile for group-based or social apps.
@Composable
fun CustomMapMarker(
imageUrl: String?,
fullName: String,
location: LatLng,
onClick: () -> Unit
) {
val markerState = remember { MarkerState(position = location) }
val shape = RoundedCornerShape(20.dp, 20.dp, 20.dp, 0.dp)
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.allowHardware(false)
.build()
)
MarkerComposable(
keys = arrayOf(fullName, painter.state),
state = markerState,
title = fullName,
anchor = Offset(0.5f, 1f),
onClick = {
onClick()
true
}
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(shape)
.background(Color.LightGray)
.padding(4.dp),
contentAlignment = Alignment.Center
) {
if (!imageUrl.isNullOrEmpty()) {
Image(
painter = painter,
contentDescription = "Profile Image",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
Text(
text = fullName.take(1).uppercase(),
color = Color.White,
style = MaterialTheme.typography.body2,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
Step 3: Add the Custom Marker to Google Maps
With our marker composable ready, initialize Google Maps in your app and add the custom profile marker:
@Composable
fun SimpleMap() {
val latLng = LatLng( -33.8568, 151.2153) // Any Coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(latLng, 14f) // Set zoom level
}
val mapProperties = remember {
MapProperties(
mapType = MapType.HYBRID,
)
}
GoogleMap(
modifier = Modifier.fillMaxSize(), // Fill the entire screen
cameraPositionState = cameraPositionState,
properties = mapProperties
) {
CustomMapMarker(
imageUrl = "https://gravatar.com/avatar/27205e5c51cb03f862138b22bcb5dc20f94a342e744ff6df1b8dc8af3c865109?s=2048",
fullName = "Michael",
location = latLng,
onClick = { }
)
}
}
And here is the result:
Step 4: Expand the Custom Marker on Click
Now, let’s display the user’s name alongside their profile image when the marker is clicked. We’ll add an expandMarker
variable to track the state of the marker. This variable should be added in the keys
array of MarkerComposable
.
Here’s the updated CustomMapMarker
composable:
@Composable
fun CustomMapMarker(
imageUrl: String?,
fullName: String,
location: LatLng,
onClick: () -> Unit
) {
val markerState = remember { MarkerState(position = location) }
val shape = RoundedCornerShape(20.dp, 20.dp, 20.dp, 0.dp)
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.allowHardware(false)
.build()
)
var expandMarker by remember {
mutableStateOf(false)
}
MarkerComposable(
keys = arrayOf(fullName, painter.state, expandMarker),
state = markerState,
title = fullName,
anchor = Offset(0.5f, 1f),
onClick = {
onClick()
expandMarker = !expandMarker
true
}
) {
Box(
modifier = Modifier
.size(if (expandMarker) 100.dp else 48.dp)
.clip(shape)
.background(Color.White)
.padding(4.dp),
contentAlignment = Alignment.Center
) {
if (!imageUrl.isNullOrEmpty()) {
if (!expandMarker) {
Image(
painter = painter,
contentDescription = "Profile Image",
modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(16.dp)),
contentScale = ContentScale.Crop
)
} else {
Column(modifier = Modifier.fillMaxSize()) {
Image(
painter = painter,
contentDescription = "Profile Image",
modifier = Modifier.fillMaxSize().weight(1f).clip(RoundedCornerShape(16.dp)),
contentScale = ContentScale.Crop
)
Text(
text = fullName,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
} else {
Text(
text = fullName.take(1).uppercase(),
color = Color.White,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
This setup will display each marker with a unique, personalized image, which can be beneficial in apps that require quick identification of individual locations.
Use Case in Action: GroupTrack
We applied this feature in an open-source app called GroupTrack, which provides real-time location sharing within groups. Each group member’s location is represented by their profile image on the map, making it easy for users to identify who is where. GroupTrack also includes features like group messaging, geofences, and custom location-based notifications, all available on GitHub.
Conclusion
Adding custom profile markers in Jetpack Compose can greatly improve the usability and aesthetic of maps in your app. Whether you’re building an app similar to GroupTrack or a different type of social/location-based app, this approach offers a straightforward way to personalize your map’s functionality.