Skip to content

Commit 14f74b4

Browse files
lyydikoinatiginfo
authored andcommitted
Add edge-to-edge layout examples for Android (#9711)
## Summary - Add edge-to-edge layout example to examples app with system insets handling - Add edge-to-edge layout example to Compose app with system insets handling - Fix ExampleScaffold in compose-app to prevent TopAppBar overlap with status bar ### NB: - Target API: For simplicity, these examples target Android 15+ (API 35). Starting with this version, edge-to-edge display is enforced by default for all apps (see: [Android 15 Behavior Changes (https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge)). - Backward Compatibility: I have included links to official Google documentation for supporting edge-to-edge on apps targeting API 34 or lower. ## Details ### New Examples Added edge-to-edge examples demonstrating handling of system insets (status bars, navigation bars, display cutouts) to prevent map ornaments from being obscured: Both examples include: - System bar appearance configuration -- dark icons, transparent navigation bar (for 3 buttons navigation). - Display cutout handling - Proper margin application to all map ornaments (logo, attribution, compass, scale bar) - Comprehensive documentation explaining Android 15+ edge-to-edge enforcement ### Compose App Fix Updated `ExampleScaffold.kt` to handle edge-to-edge layout properly to prevent status bar overlap with app bar This fixes the issue where the TopAppBar was being obscured by the system status bar in edge-to-edge mode, which is enforced by default on Android 15 (API 35+): Before the fix: <img width="892" height="772" alt="Screenshot 2026-01-30 at 14 25 24" src="https://github.com/user-attachments/assets/dd26d37a-deb3-4dc2-b0ef-8a258ce568c8" /> After fix: <img width="1431" height="560" alt="Screenshot 2026-01-30 at 14 28 06" src="https://github.com/user-attachments/assets/a74d4ed4-531c-4108-8d71-bbdc0cd52c25" /> ## How to test: - Launch both edge-to-edge examples on devices with: - Android 15+ (edge-to-edge enforced) - Various display cutout configurations (notches, punch-holes) - 3-button vs gesture navigation - Verify map ornaments are not obscured by system UI - Verify TopAppBar in compose-app examples is properly positioned below status bar Android 16 App: NB: screen recording is done with Pixel 3 emulator, which has camera cut out left top corner, which adds extra inset: <img width="380" height="778" alt="Screenshot 2026-02-02 at 9 26 04" src="https://github.com/user-attachments/assets/a0c18ff7-b476-4a72-b431-22cdc775fd9b" /> [e-t-e-app.webm](https://github.com/user-attachments/assets/484fdb65-ce98-4e1e-91c5-450e577384a5) Android 16 Compose App: NB: screen recording is done with Pixel 3 emulator, which has camera cut out left top corner, which adds extra inset. <img width="384" height="779" alt="Screenshot 2026-02-02 at 9 25 31" src="https://github.com/user-attachments/assets/d4f4115b-9531-4e70-b26f-2c4d8a119fdf" /> [e-t-e-compose.webm](https://github.com/user-attachments/assets/567e91e5-bcbd-4657-87cd-66aea82db09d) cc @mapbox/maps-android --------- Co-authored-by: natiginfo <natig.babayev@mapbox.com> GitOrigin-RevId: ddfa3de08dbf7ba3c6e43a8b72338002c3a1b477
1 parent a8e11a4 commit 14f74b4

10 files changed

Lines changed: 363 additions & 18 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,5 +1481,17 @@
14811481
android:name="android.support.PARENT_ACTIVITY"
14821482
android:value=".examples.Interactive3DModelSourceActivity" />
14831483
</activity>
1484+
<activity android:name=".examples.EdgeToEdgeActivity"
1485+
android:description="@string/description_edge_to_edge"
1486+
android:label="@string/activity_edge_to_edge"
1487+
android:exported="true"
1488+
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
1489+
<meta-data
1490+
android:name="@string/category"
1491+
android:value="@string/category_basic" />
1492+
<meta-data
1493+
android:name="android.support.PARENT_ACTIVITY"
1494+
android:value=".ExampleOverviewActivity" />
1495+
</activity>
14841496
</application>
14851497
</manifest>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.mapbox.maps.testapp.examples
2+
3+
import android.os.Build
4+
import android.os.Bundle
5+
import android.util.TypedValue
6+
import androidx.appcompat.app.AppCompatActivity
7+
import androidx.core.view.ViewCompat
8+
import androidx.core.view.WindowCompat
9+
import androidx.core.view.WindowInsetsCompat
10+
import com.mapbox.geojson.Point
11+
import com.mapbox.maps.CameraOptions
12+
import com.mapbox.maps.Style
13+
import com.mapbox.maps.plugin.attribution.attribution
14+
import com.mapbox.maps.plugin.compass.compass
15+
import com.mapbox.maps.plugin.logo.logo
16+
import com.mapbox.maps.plugin.scalebar.scalebar
17+
import com.mapbox.maps.testapp.databinding.ActivityEdgeToEdgeBinding
18+
19+
/**
20+
* This example shows how to handle system insets (status bar, navigation bar, display cutouts)
21+
* in the Edge-to-Edge layout to prevent map ornaments from being obscured.
22+
*
23+
* Key concepts:
24+
* - Combine WindowInsetsCompat.Type.systemBars() and WindowInsetsCompat.Type.displayCutout()
25+
* - Apply insets to map ornaments (logo, attribution, compass, scale bar)
26+
* - Configure system bar appearance.
27+
*
28+
* Note: Starting with Android 15 (API 35), edge-to-edge is enforced by default for all apps
29+
* targeting API 35+. See:
30+
* https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge
31+
*
32+
* For apps targeting API 34 or lower, you need to explicitly enable edge-to-edge by calling
33+
* WindowCompat.setDecorFitsSystemWindows(window, false) or WindowCompat.enableEdgeToEdge(). See:
34+
* https://developer.android.com/develop/ui/views/layout/edge-to-edge#enable-edge-to-edge-display
35+
*/
36+
class EdgeToEdgeActivity : AppCompatActivity() {
37+
38+
private lateinit var binding: ActivityEdgeToEdgeBinding
39+
40+
override fun onCreate(savedInstanceState: Bundle?) {
41+
super.onCreate(savedInstanceState)
42+
43+
binding = ActivityEdgeToEdgeBinding.inflate(layoutInflater)
44+
setContentView(binding.root)
45+
// Configure system bar appearance
46+
configureSystemBarAppearance()
47+
48+
binding.mapView.mapboxMap.apply {
49+
loadStyle(Style.STANDARD)
50+
setCamera(
51+
CameraOptions.Builder()
52+
.center(Point.fromLngLat(LONGITUDE, LATITUDE))
53+
.zoom(ZOOM)
54+
.build()
55+
)
56+
}
57+
58+
setupEdgeToEdgeInsets()
59+
}
60+
61+
/**
62+
* Set up window insets handling for edge-to-edge layout.
63+
*
64+
* This method applies system insets to map ornaments to ensure they don't overlap
65+
* with system UI elements like status bar, navigation bar or display cutouts.
66+
*/
67+
private fun setupEdgeToEdgeInsets() {
68+
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
69+
val systemInsets = insets.getInsets(
70+
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
71+
)
72+
73+
val padding4Dp = dpToPx(4f)
74+
val padding8Dp = dpToPx(8f)
75+
val logoSize = dpToPx(82f)
76+
77+
with(binding.mapView.logo) {
78+
marginLeft = systemInsets.left.toFloat() + padding4Dp
79+
marginTop = systemInsets.top.toFloat()
80+
marginRight = systemInsets.right.toFloat()
81+
marginBottom = systemInsets.bottom.toFloat() + padding4Dp
82+
}
83+
84+
with(binding.mapView.attribution) {
85+
marginLeft = systemInsets.left.toFloat() + logoSize + padding8Dp
86+
marginTop = systemInsets.top.toFloat()
87+
marginRight = systemInsets.right.toFloat() + padding4Dp
88+
marginBottom = systemInsets.bottom.toFloat() + padding4Dp
89+
}
90+
91+
with(binding.mapView.compass) {
92+
marginLeft = systemInsets.left.toFloat()
93+
marginTop = systemInsets.top.toFloat() + padding8Dp
94+
marginRight = systemInsets.right.toFloat() + padding8Dp
95+
marginBottom = systemInsets.bottom.toFloat()
96+
}
97+
98+
with(binding.mapView.scalebar) {
99+
marginLeft = systemInsets.left.toFloat() + padding8Dp
100+
marginTop = systemInsets.top.toFloat() + padding8Dp
101+
marginRight = systemInsets.right.toFloat()
102+
marginBottom = systemInsets.bottom.toFloat()
103+
}
104+
105+
insets
106+
}
107+
}
108+
109+
/**
110+
* Configure system bar appearance for edge-to-edge display.
111+
*
112+
* This example uses dark system bar icons (for better visibility on light map backgrounds).
113+
* For theme-aware icon colors or other customizations, see:
114+
* https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bar-icons
115+
*/
116+
@Suppress("DEPRECATION")
117+
private fun configureSystemBarAppearance() {
118+
// Disable automatic contrast enforcement for true transparency (API 29+)
119+
// Note: This only affects 3-button navigation (removes scrim). Gesture navigation is unaffected.
120+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
121+
window.isNavigationBarContrastEnforced = false
122+
window.isStatusBarContrastEnforced = false
123+
}
124+
125+
// Set dark system bar icons (more visible on light map backgrounds)
126+
WindowCompat.getInsetsController(window, window.decorView).apply {
127+
isAppearanceLightStatusBars = true // true = dark icons
128+
isAppearanceLightNavigationBars = true
129+
}
130+
}
131+
132+
private fun dpToPx(dp: Float): Float {
133+
return TypedValue.applyDimension(
134+
TypedValue.COMPLEX_UNIT_DIP,
135+
dp,
136+
resources.displayMetrics
137+
)
138+
}
139+
140+
companion object {
141+
private const val LATITUDE = 40.7128
142+
private const val LONGITUDE = -74.0060
143+
private const val ZOOM = 12.0
144+
}
145+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="match_parent">
6+
7+
<com.mapbox.maps.MapView
8+
android:id="@+id/mapView"
9+
android:layout_width="match_parent"
10+
android:layout_height="match_parent"
11+
app:layout_constraintBottom_toBottomOf="parent"
12+
app:layout_constraintEnd_toEndOf="parent"
13+
app:layout_constraintStart_toStartOf="parent"
14+
app:layout_constraintTop_toTopOf="parent" />
15+
16+
</androidx.constraintlayout.widget.ConstraintLayout>

app/src/main/res/values/example_descriptions.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,5 @@
117117
<string name="description_data_join">Showcase join JSON data with vector tiles and style it.</string>
118118
<string name="description_3d_model_source_interactions_label">3D model with source-driven interactions</string>
119119
<string name="description_3d_model_source_interactions">Use a model layer and source to interactively change material and orientation of model parts.</string>
120-
120+
<string name="description_edge_to_edge">Showcase edge-to-edge layout with proper handling of system insets (system bars, navigation bars, and display cutouts).</string>
121121
</resources>

app/src/main/res/values/example_titles.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,5 @@
117117
<string name="activity_color_theme">Color theme example</string>
118118
<string name="activity_elevated_line">Elevated line example</string>
119119
<string name="activity_join_data">Join local JSON data with vector tiles</string>
120+
<string name="activity_edge_to_edge">Edge-to-Edge layout</string>
120121
</resources>

compose-app/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,16 @@
409409
android:name="@string/category"
410410
android:value="@string/category_3D" />
411411
</activity>
412+
<activity
413+
android:name=".examples.basic.EdgeToEdgeActivity"
414+
android:description="@string/description_edge_to_edge"
415+
android:exported="true"
416+
android:label="@string/activity_edge_to_edge"
417+
android:parentActivityName=".ExampleOverviewActivity">
418+
<meta-data
419+
android:name="@string/category"
420+
android:value="@string/category_basic" />
421+
</activity>
412422
</application>
413423

414424
</manifest>

compose-app/src/main/java/com/mapbox/maps/compose/testapp/ExampleScaffold.kt

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
package com.mapbox.maps.compose.testapp
22

33
import android.app.Activity
4+
import androidx.compose.foundation.layout.Column
45
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.WindowInsets
8+
import androidx.compose.foundation.layout.statusBars
9+
import androidx.compose.foundation.layout.windowInsetsPadding
510
import androidx.compose.material.FabPosition
611
import androidx.compose.material.Icon
712
import androidx.compose.material.IconButton
13+
import androidx.compose.material.MaterialTheme
814
import androidx.compose.material.Scaffold
915
import androidx.compose.material.SnackbarHost
1016
import androidx.compose.material.SnackbarHostState
17+
import androidx.compose.material.Surface
1118
import androidx.compose.material.Text
1219
import androidx.compose.material.TopAppBar
1320
import androidx.compose.material.icons.Icons
1421
import androidx.compose.material.icons.filled.ArrowBack
22+
import androidx.compose.material.primarySurface
1523
import androidx.compose.runtime.Composable
1624
import androidx.compose.runtime.remember
1725
import androidx.compose.ui.Modifier
1826
import androidx.compose.ui.platform.LocalContext
27+
import androidx.compose.ui.unit.dp
1928

2029
/**
2130
* Customised Scaffold to be applied by all the activities. It adds the following UI elements to the
2231
* UI:
2332
* * TopAppBar that shows the label for current activity.
2433
* * Navigation icon that dismisses current activity, if the parent activity exists.
34+
*
35+
* Note: Edge-to-edge is enabled by default on Android 15 (API 35+).
36+
* The TopAppBar extends behind the status bar with internal padding to avoid content overlap.
37+
* See: https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge
2538
*/
2639
@Composable
2740
public fun ExampleScaffold(
@@ -45,23 +58,25 @@ public fun ExampleScaffold(
4558
floatingActionButton = floatingActionButton,
4659
floatingActionButtonPosition = floatingActionButtonPosition,
4760
topBar = {
48-
TopAppBar(
49-
title = {
50-
Text(text = name)
51-
},
52-
navigationIcon = if (hasParentActivity) {
53-
{
54-
IconButton(onClick = { activity?.finish() }) {
55-
Icon(
56-
imageVector = Icons.Filled.ArrowBack,
57-
contentDescription = "Back"
58-
)
59-
}
60-
}
61-
} else {
62-
null
61+
// Surface provides elevation and background for the entire top bar area including status bar
62+
Surface(
63+
color = MaterialTheme.colors.primarySurface,
64+
elevation = 4.dp
65+
) {
66+
Column(Modifier.windowInsetsPadding(WindowInsets.statusBars)) {
67+
TopAppBar(
68+
title = { Text(text = name) },
69+
navigationIcon = if (hasParentActivity) {
70+
{
71+
IconButton(onClick = { activity?.finish() }) {
72+
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
73+
}
74+
}
75+
} else null,
76+
elevation = 0.dp // Prevent TopAppBar shadow (Surface provides elevation)
77+
)
6378
}
64-
)
79+
}
6580
},
6681
bottomBar = bottomBar,
6782
content = content

0 commit comments

Comments
 (0)