Skip to content

fix(UiControls): prevent 3D map UI controls from overlapping status bar - #87

Open
LoyalAbbas wants to merge 2 commits into
mainfrom
fix/ui-controls-status-bar-overlap
Open

LoyalAbbas wants to merge 2 commits into
mainfrom
fix/ui-controls-status-bar-overlap

Conversation

@LoyalAbbas

Copy link
Copy Markdown
Collaborator

Summary

Fixes top-anchored Maps 3D SDK UI controls (ControlPlacement.LEADING and ControlPlacement.TRAILING) and the Quick Settings FAB overlapping the system status bar and camera cutout on full-screen edge-to-edge map screens across Kotlin Views, Java Views, and Jetpack Compose.

Problem & Root Cause

  • When full-screen 3D maps draw edge-to-edge behind the status bar (including sticky immersive mode), the SDK's top UI control containers default to a 16dp top margin, placing them inside the status bar / display cutout region.
  • Subsequent calls to setUiControlsPlacement(...) (such as on onResume() or when toggling controls) also reset the inner Play Services delegate LinearLayout top margins back to 16dp.

Changes

  • Common (Map3DSettings, Map3DQuickSettingsButton, Map3DQuickSettingsWidget):
    • Added Map3DSettings.applyStatusBarInsetsToMapView(...) and findMap3DView(...) (without reflection) to offset top leading and trailing SDK control containers by statusBarOverlap + 16dp on full-screen maps while keeping 0 extra offset when the map sits below a non-overlay toolbar (top_bar).
    • Attached layout listeners to nested delegate ViewGroups and top-aligned LinearLayouts so SDK margin resets on setUiControlsPlacement are immediately corrected.
    • Updated resolveStatusBarHeightPx to read getInsetsIgnoringVisibility(statusBars() or displayCutout()) so offsets hold in sticky immersive mode.
    • Added window insets listening on Map3DQuickSettingsButton for standalone activities (PathFollowingActivity, HelloMapActivity).
    • Added first-frame status bar overlap fallback and uiControlsOverride support in Map3DQuickSettingsWidget, plus automatic inset protection in rememberMap3DSessionSettings.
  • Kotlin & Java Views (SampleBaseActivity, UiControlsActivity):
    • Wired status bar inset application in SampleBaseActivity before onQuickSettingsChanged callbacks.
    • Re-applied status bar insets and Quick Settings FAB alignment after direct setUiControlsPlacement calls in Kotlin and Java UiControlsActivity.
  • Jetpack Compose (UiControlsActivity):
    • Re-applied status bar insets after direct SDK control updates and passed local uiControlsOverride to Map3DQuickSettingsWidget so the Quick Settings FAB stays 8dp below active trailing controls.
  • Tests (Map3DStatusBarInsetsTest):
    • Added Robolectric unit tests verifying top leading/trailing container offsets, bottom container preservation, delayed child container additions, and recovery when nested delegate containers reset topMargin.

@LoyalAbbas
LoyalAbbas requested review from dkhawk and kikoso and removed request for dkhawk October 16, 2026 09:31

@kikoso kikoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @LoyalAbbas ! Thanks for looking into this. My main concern is that we are reaching into the SDK's internal view hierarchy (any top-aligned LinearLayout inside Map3DView), which can break silently with any SDK release, and it is quite a lot of code for a samples repo. Have we raised this with the SDK team? Ideally Map3DView would respect insets or expose a padding API. If we keep the workaround for now, a // Workaround: comment with the bug link would help. Can you take a look at the comments? Thanks!

explicitStatusBarTop: Int? = null,
) {
if (explicitStatusBarTop != null && explicitStatusBarTop > 0) {
mapView.setTag(R.id.top_bar, explicitStatusBarTop)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are reusing R.id.top_bar and R.id.map3dView as tag keys here (and in line 301), which is confusing to read since both are also real view IDs looked up in resolveStatusBarOverlapPx. Could we declare dedicated keys in an ids.xml instead (e.g. map3d_status_bar_top_tag, map3d_insets_listener_tag)?

mapView: ViewGroup,
explicitStatusBarTop: Int? = null,
) {
if (explicitStatusBarTop != null && explicitStatusBarTop > 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only cache the value when it is > 0, once a positive value is stored there is no way to bring it back to 0: passing an explicit 0 (as bindWindowInsetsAndMapHierarchy does) just falls back to the old tag. Should we clear the tag when 0 is passed, or do I miss anything?

if (view.getTag(R.id.map3dView) == true) return
view.setTag(R.id.map3dView, true)

view.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This listener ends up registered on every nested ViewGroup of the map, and each layout pass on any of them walks the whole Map3DView subtree again (possibly triggering another layout). Would it make sense to register it only on the root map view, or only on the containers we actually offset?

}

if (view === rootMapView) {
rootMapView.setOnHierarchyChangeListener(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ViewGroup only supports one OnHierarchyChangeListener, so this silently replaces anything the SDK may have set on Map3DView internally. Being this an SDK-owned view, I am a bit wary of it. Could we rely on the layout listener alone?

}
if (lp != null && isTopAligned) {
ensureLayoutListener(child, rootMapView)
val effectiveBase = if (child.paddingTop > 0) 0 else baseMarginPx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to understand it: why does a positive paddingTop mean we should drop the base margin? Also, the 16dp base assumes the SDK's internal default, so if that changes we would be forcing 16dp onto every top LinearLayout, even when there is no overlap.

val overlapPx = if (view.isInEditMode) {
0
} else {
val mapView = Map3DSettings.findMap3DView(view.rootView)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs on every recomposition and walks the whole hierarchy from rootView. It also reads view geometry rather than state, so it will not recompose when the map gets laid out or the insets change. Could we move it into a remember/LaunchedEffect keyed on the insets?

*/
@JvmOverloads
fun updateTopMarginForUiControls(uiControls: Map3DUiControls = getSettings().uiControls) {
applyStatusBarInsetsToSiblingMapView()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateTopMarginForUiControls now also changes the map's internal containers, which is a surprising side effect for a public method with this name. Together with lines 60, 208 and 209, we apply it three or four times per settings change. What about removing it from here and calling it explicitly only where needed?

).apply { topMargin = baseMarginPx }
mapView.addView(delayedTrailingControls, trailingLp)

// Simulate explicit re-application (as triggered by onChildViewAdded / onMapReady)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this explicit call we are not really testing the onChildViewAdded path: the test would pass even without the hierarchy listener. Could we remove it and just idle the looper? Also, since this tests common code, shouldn't it live in common/src/test next to Map3DSettingsControllerTest?

@LoyalAbbas

Copy link
Copy Markdown
Collaborator Author

Hi @LoyalAbbas ! Thanks for looking into this. My main concern is that we are reaching into the SDK's internal view hierarchy (any top-aligned LinearLayout inside Map3DView), which can break silently with any SDK release, and it is quite a lot of code for a samples repo. Have we raised this with the SDK team? Ideally Map3DView would respect insets or expose a padding API. If we keep the workaround for now, a // Workaround: comment with the bug link would help. Can you take a look at the comments? Thanks!

Thanks for the thorough review, @kikoso! Completely agree that Map3DView should handle window insets or expose a padding API natively—I will discuss this with the SDK team so we can support it properly in the SDK. For now, I've added a // Workaround: comment in Map3DSettings.kt and resolved all your inline comments in the latest commit.

LoyalAbbas added a commit that referenced this pull request Oct 19, 2026
- Add dedicated view tag IDs in common `ids.xml` instead of reusing view layout IDs
- Support clearing cached status bar offset when an explicit `0` is passed
- Remove `OnHierarchyChangeListener` and scope `OnLayoutChangeListener` to root map view and offset control containers
- Preserve each control container's original SDK `topMargin` instead of assuming a hardcoded 16dp base margin
- Move Compose status bar overlap calculation into a `DisposableEffect` with layout listener state
- Remove sibling map mutation side effect from `Map3DQuickSettingsButton.updateTopMarginForUiControls`
- Move `Map3DStatusBarInsetsTest` to `:Maps3DSamples:ApiDemos:common` and test delayed child layout listener directly
- Add dedicated view tag IDs in common `ids.xml` instead of reusing view layout IDs
- Support clearing cached status bar offset when an explicit `0` is passed
- Remove `OnHierarchyChangeListener` and scope `OnLayoutChangeListener` to root map view and offset control containers
- Preserve each control container's original SDK `topMargin` instead of assuming a hardcoded 16dp base margin
- Move Compose status bar overlap calculation into a `DisposableEffect` with layout listener state
- Remove sibling map mutation side effect from `Map3DQuickSettingsButton.updateTopMarginForUiControls`
- Move `Map3DStatusBarInsetsTest` to `:Maps3DSamples:ApiDemos:common` and test delayed child layout listener directly
@LoyalAbbas
LoyalAbbas force-pushed the fix/ui-controls-status-bar-overlap branch from acdaba5 to e75d5a4 Compare October 19, 2026 07:59
@LoyalAbbas
LoyalAbbas requested a review from kikoso October 19, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants