Skip to main content
Version: 6.0

Dynamic Focus

Using Mappedin SDK for Android with your own map requires a Pro license. Try a demo map for free or refer to the Pricing page for more information.

A single venue may include multiple nearby buildings, each contained within a FloorStack. Dynamic Focus enables seamless exploration across these buildings by revealing indoor content as the camera pans over each structure. There are two ways to implement Dynamic Focus.

1. Dynamic Focus Class

The first approach is to use the DynamicFocus API. It provides a simple interface for implementing Dynamic Focus, allowing automatic switching between building facades and building maps as they move into and out of focus when the map is panned.

2. Dynamic Focus using MapView.updateState

The second approach is to use the MapView.updateState() method to update the map view when the camera pans over a building. This approach is more flexible and allows for more control over Dynamic Focus behavior, but it requires more code to implement. This can allow for custom behaviour, such as showing the building map of different elevations. This can be useful when floors of nearby buildings don't share the same elevation, such as when level 2 of one building is aligned with level 3 of another building. It could also allow for showing and hiding markers or labels when focus is transitioned to a new building.

Mappedin Dynamic Focus

Mappedin Dynamic Focus with Auto Focus Enabled

Implementing the Mappedin Dynamic Focus API

The Mappedin Dynamic Focus API offers a simple API for implementing Dynamic Focus. If custom behaviour is required refer to the Implementing Dynamic Focus using MapView section.

tip

A complete example demonstrating the Dynamic Focus API can be found in the Mappedin Android Github repo: DynamicFocusDemoActivity.kt

Usage

tip

Note that the MapView class instantiates the DynamicFocus class and exposes it as MapView.dynamicFocus. Use MapView.dynamicFocus to utilize DynamicFocus' methods.

Dynamic Focus can be enabled by calling DynamicFocus.enable(). When enabled, the Dynamic Focus will automatically transition the currently visible map from one building to the next as the camera pans over it.

The following example demonstrates how to enable Dynamic Focus with auto focus enabled, setting the floor on focus and specifying the indoor and outdoor zoom thresholds.

val options =
DynamicFocusOptions(
autoFocus = true,
setFloorOnFocus = true,
indoorZoomThreshold = 17.0,
outdoorZoomThreshold = 17.0,
)

mapView.dynamicFocus.enable(options) { result ->
result.fold(
onSuccess = {
Log.d("Dynamic Focus enabled")
},
onFailure = { error ->
Log.e("Error enabling: ${error.message}")
},
)
}

To disable Dynamic Focus, call DynamicFocus.disable().

mapView.dynamicFocus.disable { _ ->
Log.d("Dynamic Focus disabled")
}

Dynamic Focus has a number of options that can be enabled either at instantiation or at any runtime by calling DynamicFocus.updateState.

Auto Focus

Dynamic Focus' auto focus provides default behavior to transition the currently visible map from one building to the next as the camera pans over it. The default behavior can be overridden by setting the DynamicFocusOptions.autoFocus option to false, allowing the developer to control the focus manually.

Implementing Dynamic Focus using MapView

Dynamic Focus can be implemented using the MapView.updateState method. This approach is more flexible and allows for full customization of Dynamic Focus behavior, but it requires more code to implement. For a simple implementation of Dynamic Focus, refer to the Mappedin Dynamic Focus API section.

tip

A complete example demonstrating a Dynamic Focus implementation using MapView.updateState can be found in the Mappedin Android Github repo: DynamicFocusManualDemoActivity.kt

Implementing Dynamic Focus using MapView.updateState requires the app to switch the visibility of a building Facade and Floor when the camera pans over a building. The Facade represents the look of the building from the outside, with its roof on. The Floor represents the look of the building from the inside, showing the indoor map.

Listen for Changes to Facades In view

The FacadesInViewChange event is emitted when the facades in view change. This event is emitted when the camera pans over a building. The event provides the list of facades in view. The app can use this event to update the visibility of the facades and floors. The first Facade in the array contains the one that is most centered in view.

// When facades come into view, switch to show that building's interior
mapView.on(Events.FacadesInViewChange) { event ->
event?.let { payload ->
logEvent("facades-in-view-change: ${payload.facades.size} facade(s)")

// If a facade is in view, switch to that building
if (payload.facades.isNotEmpty()) {
val facade = payload.facades.first()
val floorStackId = facade.floorStack

// Only switch if it's a different building
if (floorStackId != null && floorStackId != currentSelectedFloorStackId) {
val floorStack = allFloorStacks.find { it.id == floorStackId }
floorStack?.let {
Log.d("DynamicFocusManual", "Facade in view - switching to building: ${it.name}")
// Don't focus camera - user is already panning to this location
switchToBuilding(it, focusCamera = false)
}
}
} else {
// No facades in view - switch to outdoor
// Don't focus camera - user panned away, keep camera where it is
val outdoorFloorStack = allFloorStacks.find { it.type == FloorStack.FloorStackType.OUTDOOR }
if (outdoorFloorStack != null && outdoorFloorStack.id != currentSelectedFloorStackId) {
Log.d("DynamicFocusManual", "No facades in view - switching to outdoor")
switchToBuilding(outdoorFloorStack, focusCamera = false)

// Update the building spinner to reflect the change
val buildingIndex = allFloorStacks.indexOf(outdoorFloorStack)
runOnUiThread {
buildingSpinner.onItemSelectedListener = null
if (buildingIndex >= 0) {
buildingSpinner.setSelection(buildingIndex)
}
buildingSpinner.post { buildingSpinner.onItemSelectedListener = buildingSpinnerListener }
}
}
}
}
}

The following code demonstrates how to switch to a building by manually managing facades and floors. This does not call setFloorStack or setFloor for buildings - instead it manages visibility of floors and facades directly while staying on the outdoor floor. This allows facades to remain rendered while showing building interiors. If the user has previously selected a floor, it will be restored when the building is switched to. If the user hasn't previously selected a floor a floor matching the current elevation will be shown if available, if not the default floor will be shown.

/**
* Switch to viewing a building by manually managing facades and floors.
* This does NOT call setFloorStack or setFloor for buildings - instead it manages
* visibility of floors and facades directly while staying on the outdoor floor.
* This allows facades to remain rendered while showing building interiors.
*
* @param floorStack The floor stack to switch to
* @param focusCamera Whether to focus the camera on the target. Set to false when
* switching due to panning (facades-in-view-change) to avoid snapping back.
*/
private fun switchToBuilding(
floorStack: FloorStack,
focusCamera: Boolean = true,
) {
currentSelectedFloorStackId = floorStack.id

Log.d("DynamicFocusManual", "switchToBuilding: ${floorStack.name} (${floorStack.id}) focusCamera=$focusCamera")

// Check if this is an Outdoor-type floor stack
val isOutdoor = floorStack.type == FloorStack.FloorStackType.OUTDOOR

if (isOutdoor) {
// Switching to Outdoor - close the previously open facade (if any)
Log.d("DynamicFocusManual", " Switching to Outdoor view")

// Only close the facade that was previously opened (not all facades)
currentlyOpenFacadeId?.let { openFacadeId ->
val facadeToClose = allFacades.find { it.id == openFacadeId }
facadeToClose?.let { closeFacade(it) }
}
currentlyOpenFacadeId = null

// Set floor to the outdoor floor
floorStack.defaultFloor?.let { defaultFloorId ->
val floor = allFloors.find { it.id == defaultFloorId }
floor?.let {
mapView.setFloor(it.id)
// Only focus if explicitly requested (e.g., from picker selection)
if (focusCamera) {
mapView.camera.focusOn(it)
}
}
}
} else {
// Switching to a Building - open its facade (hide it) and show its floors
Log.d("DynamicFocusManual", " Switching to Building: ${floorStack.name}")

// Find the facade for the new building
val newFacade = allFacades.find { it.floorStack == floorStack.id }

// Close the previously open facade (if different from the new one)
currentlyOpenFacadeId?.let { openFacadeId ->
if (openFacadeId != newFacade?.id) {
val facadeToClose = allFacades.find { it.id == openFacadeId }
facadeToClose?.let { closeFacade(it) }
}
}

// Open the new building's facade (if it has one)
if (newFacade != null) {
openFacade(newFacade)
currentlyOpenFacadeId = newFacade.id
} else {
// Building has no facade - clear the tracking variable to avoid
// attempting to close an already-closed facade on subsequent transitions
currentlyOpenFacadeId = null
// Still need to show floors even without a facade, otherwise
// the building interior would be invisible
showFloors(floorStack)
}

// Determine which floor to show:
// 1. Use stored preference for this building (highest priority)
// 2. Try to find a floor matching current elevation
// 3. Fall back to default floor
val floorsInStack = floorStack.floors.mapNotNull { floorId -> allFloors.find { it.id == floorId } }
val storedPref = floorToShowByBuilding[floorStack.id]
val elevationMatch = floorsInStack.find { it.elevation == currentElevation }
val defaultFloor = allFloors.find { it.id == floorStack.defaultFloor }

val floorToShow = storedPref ?: elevationMatch ?: defaultFloor

floorToShow?.let {
// Store this as the preference for this building
floorToShowByBuilding[floorStack.id] = it
// Update the global elevation tracker
currentElevation = it.elevation

// NOTE: We do NOT call mapView.setFloor() here!
// Calling setFloor causes the SDK to internally manage facades, hiding all
// facades except the active building's. Instead, we manually manage visibility
// using updateState/animateState while staying on the outdoor floor conceptually.
// Floor visibility is handled via showFloors() called above.

// Only focus camera if explicitly requested (e.g., from picker selection)
if (focusCamera) {
mapView.camera.focusOn(it)
}
}
}
}

private fun openFacade(facade: Facade) {
// Get the FloorStack object from the facade's floorStack ID
val floorStack = allFloorStacks.find { it.id == facade.floorStack } ?: return

Log.d("DynamicFocusManual", "openFacade: ${floorStack.name} (setting opacity to 0)")

// First, show the floor we want to see
showFloors(floorStack)

// Animate the facade out (hide it to reveal interior)
mapView.animateState(
facade,
FacadeUpdateState(opacity = 0.0),
)
}

private fun closeFacade(facade: Facade) {
val floorStack = allFloorStacks.find { it.id == facade.floorStack }
Log.d("DynamicFocusManual", "closeFacade: ${floorStack?.name ?: "unknown"} (setting opacity to 1)")

// Animate the facade in (show it to hide interior)
mapView.animateState(
facade,
FacadeUpdateState(opacity = 1.0),
) { _ ->
// Hide all floors for this building after animation completes
floorStack?.floors?.forEach { floorId ->
val floor = allFloors.find { it.id == floorId }
floor?.let {
mapView.updateState(
it,
FloorUpdateState(visible = false),
)
}
}
}
}

Listen for Changes to the Floor

The FloorChange event is emitted when the floor changes, for example when the user selects a new floor from a floor selector or when they click on a navigation marker to go up or down stairs or an elevator. The event provides the new floor. The app can use this event to update the visibility of the facades and floors.

// Act on the floor-change event to update the level selector and floor visibility
mapView.on(Events.FloorChange) { event ->
event?.let { payload ->
val newFloor = payload.floor
val newFloorStackId = newFloor.floorStack?.id
Log.d("DynamicFocusManual", "floor-change: ${newFloor.name} (floorStackId=$newFloorStackId)")

// Store this floor as the preference for its floor stack
// This preserves user selections across building switches
val isOutdoorFloor = newFloor.floorStack?.type == FloorStack.FloorStackType.OUTDOOR
newFloorStackId?.let { stackId ->
floorToShowByBuilding[stackId] = newFloor
}

if (!isOutdoorFloor) {
currentElevation = newFloor.elevation
}

// Get the floor stack name for logging
val floorStackName = newFloor.floorStack?.name ?: "Unknown"
logEvent("floor-change: ${newFloor.name} ($floorStackName)")
}
}