This example demonstrates how to render a compass icon that reflects the map's heading, where 0° is north, 90° is east, 180° is south and 270° is west. Tapping the compass animates the map back to a north-up orientation. The example also shows how to enable live heading: reading the device's compass sensor so the map rotates as you physically turn the device.
Initial map rotation angle, north pointing up
Map rotation angle changed, north pointing differently
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. It then wires up the UI in setupUi() and registers the SDK listeners.
onStop stops live heading so the sensor isn't read while the app is in the background, and onDestroy clears the listeners, releases the SDK with GemSdk.release(), and exits the process.
The layout stacks the controls over a full-screen GemSurfaceView: a compass image in the top-right corner, a FloatingActionButton to toggle live heading, and a status_text strip along the bottom. All three start hidden (visibility="gone") and are revealed once the map is ready.
The compass behavior is set up in registerSdkListeners(). Once the map is ready (onDefaultMapViewCreated), the controls are shown and two links are established:
mapView.onMapAngleUpdated fires on every map rotation and rotates the compass icon to match. The angle is negated (-it) so the needle points to true north as the map turns underneath it.
Tapping the compass calls mapView.alignNorthUp(Animation(EAnimation.Linear, 300)) to animate the map back to north-up over 300 ms - but only while live heading is off, since otherwise the sensor would immediately rotate it again.
The remaining callbacks handle the lifecycle: onSdkInitFailed reports a fatal init error and closes the activity, onSurfaceChanged re-aligns the logo on resize, and SdkSettings.onApiTokenRejected warns about an invalid token.
The floating button toggles live heading. toggleLiveHeading() flips the state, updates the button's appearance and status text via renderLiveHeadingState() (a play icon when stopped, a pause icon when running), and starts or stops listening for sensor data.
startLiveHeading() creates a live data source from the device's sensors with DataSourceFactory.produceLive() and registers dataSourceListener for EDataType.Compass data. Both the "no sensor available" and "listener rejected" cases surface an error dialog. stopLiveHeading() removes the listener and releases the data source.
Each time the compass sensor delivers a new reading, dataSourceListener.onNewData runs on the SDK thread via SdkCall.postAsync. The raw heading is passed through HeadingSmoother, which keeps a rolling window of recent readings and returns their circular average (it sums the sine and cosine components and takes atan2), removing the jitter typical of raw magnetometer data. The smoothed value is applied to the map as preferences.rotationAngle, so the map turns with the device.
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. Here it also accounts for the bottom status_text strip, reserving whichever reaches higher - the navigation bar or the status text. Map access runs on the SDK thread inside SdkCall.runSynced.
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.