Adding animations to Google Maps in Jetpack Compose
You might have already seen Sinasamaki's great Jetpack Compose animation articles such as the Animated Counter in Jetpack Compose, Metaball animations and Dynamic Island in Compose. In this article Sinasamaki is sharing his animation prowess with us in how add animations in your Google Maps implementation.
The Google Maps library for Jetpack Compose was released a little over a year ago and the team did such an awesome job with it. But one area that I feel it falls short is with animations, especially since we can't use regular Composables on components such as Markers, Polylines, etc.
In this article, I will show you how I added a Compose layer over the map where I can add any Composable using map projections.
Before we begin
We need to set up google maps in our project.
- Create a project on Google cloud console
- Add package name and SHA key in the console
- Add API key to our project
<application
...
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="ENTER-API-KEY-HERE" />
...
</application>
- Add dependencies in our app's gradle file
implementation 'com.google.maps.android:maps-compose:2.10.0'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
For more info on how to set this up, visit the documentation.
Displaying a map
Using this library, we can display a map like this:
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
) {
Marker(
state = MarkerState(position = singapore),
title = "Singapore",
snippet = "Marker in Singapore"
)
}
This code gives us a map that fills the whole screen and adds a marker on a point in the screen. Let's focus on the marker itself. Within the content of the GoogleMap
we can add a number of different map objects, including a Marker
, and define certain properties.
This Marker
looks a lot like the Composable functions we have known to love, but it's not quite the same.
It is declarative so we could change its functionality the same way as other Composables. For example, if we want to hide it, We just wrap it in an if statement:
if (visible) {
Marker(...)
}
But we can't use other Composables within the GoogleMap
content. For example, adding an AnimatedVisibility
will cause a build error.
// THIS WILL FAIL!
AnimatedVisibility (visible = markerIsVisible) {
Marker()
}
When I discovered this, I really felt powerless since all the Compose animation tricks would not work here. Like Superman under the red sun of Krypton, I could do nothing but just watch the marker appear and disappear with an if statement.
Hacking animations into the map
But all hope was not lost. In order to add normal Composables to the map, I ended up building a layer that will sit right on top of the map, where we can use all the Composables we want. The solution: Map Projections We can take the state of the camera as it zooms and pans on the map and translate that to pixel coordinates. Using these coordinates, we can display regular Composables on a layer above the map.
@Stable
fun LatLng.toPx(): IntOffset {
cameraPositionState.position
return cameraPositionState.projection
?.toScreenLocation(this)
?.let { point ->
IntOffset(point.x, point.y)
} ?: IntOffset.Zero
}
This toPx
extension function returns the x,y position of the coordinates on screen. We can then use this to make a custom marker that sits on that position on screen.
Box(
modifier = Modifier
.offset { marker.toPx() }
.background(color = Color.Red, CircleShape)
.size(20.dp)
)
From here, it's smooth sailing. We can just easily wrap our custom marker with an AnimatedVisibility
Composable, or apply any other animation we need. We could also create custom markers with all the Composables available to us. The possibilities are way more than the red marker dot I came up with ;)
To make this a little easier, I created a scope that can supply the extension function with the camera state it needs.
class MapOverlayScope(
private val cameraPositionState: CameraPositionState,
) {
@Stable
fun Modifier.align(alignment: Alignment) = this.composed {
var intSize by remember { mutableStateOf(IntSize.Zero) }
onSizeChanged { intSize = it }.offset {
alignment.align(intSize, IntSize.Zero, LayoutDirection.Ltr)
}
}
@Stable
fun LatLng.toPx(): IntOffset {
cameraPositionState.position
return cameraPositionState.projection
?.toScreenLocation(this)
?.let { point ->
IntOffset(point.x, point.y)
} ?: IntOffset.Zero
}
}
And to wrap everything together is a MapOverlay
Composable that takes in a CameraPositionState
and provides a MapOverlayScope
for all our Composables to access map projections.
@Composable
fun MapOverlay(
cameraPositionState: CameraPositionState,
content: @Composable MapOverlayScope.() -> Unit
) {
Box(Modifier.fillMaxSize()) {
MapOverlayScope(cameraPositionState).content()
}
}
Conclusion
And with that, you have all the tools to build creative map UIs in compose. There are many other possibilities, such as building a road animation with a gradient.
The code for this animation plus the entire sample project is available on Github. Looking forward to seeing what you will build. Thanks for reading & good luck!
For more of Sinasamaki's animation articles, check out sinasamaki.com