Skip to main content

Apply Custom Map Style

Last updated: June 16, 2026 | 4 minutes read

This example demonstrates how to apply a custom map style to an interactive map. The style is created in the online style editor, exported as a .style file, bundled in the project's assets folder, and applied to the map from those raw bytes - no download required at runtime.

The map is fully 3D, supporting pan, pinch-zoom, pinch-rotate and tilt.

Zoomed in map with custom style
Zoomed out map with custom style

Map Display and Setup

MainActivity overrides onCreate, which inflates the view binding, calls setContentView(binding.root) for the layout in res/layout/activity_main.xml, and enables edge-to-edge drawing so the map fills the screen. It then registers the SDK listeners and checks for an internet connection (needed to load the base map data).

When the activity is destroyed, onDestroy clears the listeners, deinitializes the SDK with GemSdk.release(), and exits the process.

MainActivity.ktView on Github
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

// Keep status-bar icons light against the dark primary toolbar background.
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = false

registerSdkListeners()

if (!Util.isInternetConnected(this)) {
showDialog(getString(R.string.internet_required))
}
}

override fun onDestroy() {
super.onDestroy()
clearSdkListeners()
GemSdk.release()
exitProcess(0)
}

The layout hosts a full-screen com.magiclane.sdk.core.GemSurfaceView that renders the map.

activity_main.xmlView on Github
<com.magiclane.sdk.core.GemSurfaceView
android:id="@+id/gem_surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />

Applying the Custom Style

The style is applied as soon as the map is ready. registerSdkListeners() sets onDefaultMapViewCreated, which aligns the Magic Lane logo and then calls applyCustomAssetStyle(mapView). The other callbacks handle a failed SDK initialization, surface resizes, and a rejected API token.

MainActivity.ktView on Github
private fun registerSdkListeners() {
binding.gemSurface.onSdkInitFailed = { error ->
val errorMessage = getString(R.string.sdk_initialization_failed, GemError.getMessage(error, this))
runOnAliveUi { showDialog(errorMessage) { finish() } }
}

binding.gemSurface.onDefaultMapViewCreated = { mapView ->
// Align the Magic Lane logo with system window insets on first map creation.
updateFocusViewport()
applyCustomAssetStyle(mapView)
}

// Re-align the logo whenever the surface is resized (e.g. rotation).
binding.gemSurface.onSurfaceChanged = { _, _ ->
updateFocusViewport()
}

SdkSettings.onApiTokenRejected = {
runOnAliveUi { showDialog(getString(R.string.token_rejected_message)) }
}
}

applyCustomAssetStyle() is the core of the example. It opens the bundled .style file from the assets folder, reads its raw bytes, and applies them to the map with preferences?.setMapStyleByDataBuffer(DataBuffer(data)). Wrapping the bytes in a DataBuffer lets the SDK apply a style straight from memory, rather than by id like a downloaded style. The map redraws immediately with the custom appearance.

MainActivity.ktView on Github
// Reads the bundled .style asset and applies it to the map as a custom style.
private fun applyCustomAssetStyle(mapView: MapView?) {
val filename = "Basic_1_Oldtime_with_Elevation.style"

val inputStream = applicationContext.resources.assets.open(filename)
val data = inputStream.readBytes()
if (data.isEmpty()) return

mapView?.preferences?.setMapStyleByDataBuffer(DataBuffer(data))
}

Keeping the Logo Clear of the System Bars

Because the map draws edge-to-edge, updateFocusViewport() keeps the Magic Lane logo visible by setting the map view's focusViewport to the area free of the system window insets. Map access runs on the SDK thread inside SdkCall.runSynced.

MainActivity.ktView on Github
private fun updateFocusViewport() {
SdkCall.runSynced {
val mapView = binding.gemSurface.mapView ?: return@runSynced
val viewport = mapView.viewport ?: return@runSynced
val insets = ViewCompat.getRootWindowInsets(binding.root)?.getInsets(SYSTEM_INSET_TYPES)

val w = viewport.width
val h = viewport.height
val left = insets?.left ?: 0
val top = insets?.top ?: 0
val right = (w - (insets?.right ?: 0)).coerceAtLeast(left)
val bottom = (h - (insets?.bottom ?: 0)).coerceAtLeast(top)
mapView.preferences?.focusViewport = Rect(left, top, right, bottom)
}
}

Releasing the Listeners

clearSdkListeners() is called from onDestroy to detach every SDK callback before the activity goes away, so a late callback can never reach a destroyed activity.

MainActivity.ktView on Github
private fun clearSdkListeners() {
SdkSettings.onApiTokenRejected = {}
binding.gemSurface.apply {
onSdkInitFailed = {}
onDefaultMapViewCreated = {}
onSurfaceChanged = { _, _ -> }
}
}