Sync Multiple Maps Side by Side
This tutorial shows how to display two MapMetrics maps side by side and keep their camera positions synchronized — useful for comparing map styles, before/after views, or satellite vs. street map.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Synchronized Dual Maps
Create two maps that stay in sync:
kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style
class SyncMapsActivity : AppCompatActivity() {
private lateinit var mapViewLeft: MapView
private lateinit var mapViewRight: MapView
private var mapLeft: MapMetricsMap? = null
private var mapRight: MapMetricsMap? = null
private var isSyncing = false // prevent infinite loop
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_maps)
mapViewLeft = findViewById(R.id.mapViewLeft)
mapViewRight = findViewById(R.id.mapViewRight)
mapViewLeft.onCreate(savedInstanceState)
mapViewRight.onCreate(savedInstanceState)
// Initialize left map
mapViewLeft.getMapAsync { map ->
mapLeft = map
map.setStyle(
Style.Builder().fromUri(
"https://gateway.mapmetrics.org/styles/STYLE_A?token=YOUR_API_KEY"
)
)
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(48.8566, 2.3522))
.zoom(12.0)
.build()
setupSync()
}
// Initialize right map
mapViewRight.getMapAsync { map ->
mapRight = map
map.setStyle(
Style.Builder().fromUri(
"https://gateway.mapmetrics.org/styles/STYLE_B?token=YOUR_API_KEY"
)
)
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(48.8566, 2.3522))
.zoom(12.0)
.build()
setupSync()
}
}
private fun setupSync() {
// Only set up once both maps are ready
if (mapLeft == null || mapRight == null) return
// Sync left → right
mapLeft?.addOnCameraMoveListener {
if (isSyncing) return@addOnCameraMoveListener
isSyncing = true
mapLeft?.cameraPosition?.let { pos ->
mapRight?.moveCamera(
CameraUpdateFactory.newCameraPosition(pos)
)
}
isSyncing = false
}
// Sync right → left
mapRight?.addOnCameraMoveListener {
if (isSyncing) return@addOnCameraMoveListener
isSyncing = true
mapRight?.cameraPosition?.let { pos ->
mapLeft?.moveCamera(
CameraUpdateFactory.newCameraPosition(pos)
)
}
isSyncing = false
}
}
// Lifecycle — must forward to BOTH map views
override fun onStart() {
super.onStart()
mapViewLeft.onStart()
mapViewRight.onStart()
}
override fun onResume() {
super.onResume()
mapViewLeft.onResume()
mapViewRight.onResume()
}
override fun onPause() {
super.onPause()
mapViewLeft.onPause()
mapViewRight.onPause()
}
override fun onStop() {
super.onStop()
mapViewLeft.onStop()
mapViewRight.onStop()
}
override fun onDestroy() {
super.onDestroy()
mapViewLeft.onDestroy()
mapViewRight.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapViewLeft.onSaveInstanceState(outState)
mapViewRight.onSaveInstanceState(outState)
}
}Layout (res/layout/activity_sync_maps.xml):
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<org.maplibre.android.maps.MapView
android:id="@+id/mapViewLeft"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<View
android:layout_width="2dp"
android:layout_height="match_parent"
android:background="#333333" />
<org.maplibre.android.maps.MapView
android:id="@+id/mapViewRight"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>Vertical Split (Top/Bottom)
For portrait orientation, stack maps vertically:
xml
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.maplibre.android.maps.MapView
android:id="@+id/mapViewTop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#333333" />
<org.maplibre.android.maps.MapView
android:id="@+id/mapViewBottom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>Style Comparison Use Cases
| Left Map | Right Map | Purpose |
|---|---|---|
| Streets | Satellite | Terrain comparison |
| Light theme | Dark theme | Theme preview |
| Current data | Historical data | Time comparison |
| Default style | Custom style | Style development |
Next Steps
- Custom Styling — Map style customization
- Fly to a Location — Camera animations
- Configuration — Map options
Tip: The isSyncing flag is critical — without it, map A's move triggers map B's listener, which triggers map A's listener, creating an infinite loop. Always guard against re-entrant sync.