ylliX - Online Advertising Network
Preview and test your app’s edge-to-edge UI

Insets handling tips for Android 15’s edge-to-edge enforcement


We can use the window inset listener so that all list items, including the last list item, are padded above the navigation bar.

Figure 12. App has handled insets, but feels less immersive because content does not scroll behind system bars.
// Figure 12
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.recycler_view)
) { v, insets ->
val innerPadding = insets.getInsets(
// Notice we're using systemBars, not statusBar
WindowInsetsCompat.Type.systemBars()
// Notice we're also accounting for the display cutouts
or WindowInsetsCompat.Type.displayCutout()
// If using EditText, also add
// "or WindowInsetsCompat.Type.ime()"
// to maintain focus when opening the IME
)
v.setPadding(
innerPadding.left,
innerPadding.top,
innerPadding.right,
innerPadding.bottom)
insets
}

However, now the app looks less immersive. To get the result we want, add clipToPadding=false to ensure the last list item sits above the navigation bar and the list is visible while scrolling behind the navigation bar (and status bar).

Figure 13. App displays edge-to-edge and the last list item is fully visible. This is the result we want.
<!-- Figure 13 -->
<RecyclerView
...
android:clipToPadding="false" />

5. Don’t forget IMEs

Set android:windowSoftInputMode=”adjustResize” in your Activity’s AndroidManifest.xml entry to make room for the IME (or soft keyboard) on screen.

Handling IMEs insets in Compose

Account for the IME using Modifier.imePadding(). For example, this can help maintain focus on a TextField in a LazyColumn when the IME opens. See the Inset consumption section for a code example and explanation.

Handling IMEs insets in Views

Before targeting SDK 35, using android:windowSoftInputMode=”adjustResize” was all you needed to maintain focus on — for example — an EditText in a RecyclerView when opening an IME. With “adjustResize”, the framework treated the IME as the system window, and the window’s root views were padded so content avoids the system window.

After targeting SDK 35, you must also account for the IME using ViewCompat.setOnApplyWindowInsetsListener and WindowInsetsCompat.Type.ime() because the framework will not pad the window’s root views. See Figure 12’s code example.

6. For backward compatibility, use enableEdgeToEdge instead of setDecorFitsSystemWindows

After your app has handled insets, make your app edge-to-edge on previous Android versions. For this, use enableEdgeToEdge instead of setDecorFitsSystemWindows. The enableEdgeToEdge method encapsulates the about 100 lines of code you need to be truly backward compatible.

7. Background protect system bars only when necessary

In many cases, keep the new Android 15 defaults. The status bar and gesture navigation bar should be transparent, and three button navigation translucent after targeting SDK 35 (see Figure 1).

However, there are some cases where you wish to preserve the background color of the system bars, but the APIs to set the status and navigation bar colors are deprecated. We are planning to release an AndroidX library to support this use case. In the meantime, if your app must offer custom background protection to 3-button navigation or the status bar, you can place a composable or view behind the system bar using WindowInsets.Type#tappableElement() to get the 3-button navigation bar height or WindowInsets.Type#statusBars.

For example, to show the color of the element behind the 3-button navigation in Compose, set the window.isNavigationBarContrastEnforced property to false. Setting this property to false makes 3-button navigation fully transparent (note: this property does not affect gesture navigation).

Then, use WindowInsets.tappableElement to align UI behind insets for tappable system UI. If non-0, the user is using tappable bars, like three button navigation. In this case, draw an opaque view or box behind the tappable bars.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
window.isNavigationBarContrastEnforced = false
MyTheme {
Surface(...) {
MyContent(...)
ProtectNavigationBar()
}
}
}
}
}

// Use only if required.
@Composable
fun ProtectNavigationBar(modifier: Modifier = Modifier) {
val density = LocalDensity.current
val tappableElement = WindowInsets.tappableElement
val bottomPixels = tappableElement.getBottom(density)
val usingTappableBars = remember(bottomPixels) {
bottomPixels != 0
}
val barHeight = remember(bottomPixels) {
tappableElement.asPaddingValues(density).calculateBottomPadding()
}

Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
if (usingTappableBars) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(barHeight)
)
}
}
}

The following tips apply only for apps that use Jetpack Compose. See additional Compose-related tips in this video: Edge-to-edge and insets | Compose Tips.

8. Use Scaffold’s PaddingValues

For Compose, use Scaffold instead of Surface to organize your app’s UI with TopAppBar, BottomAppBar, NavigationBar, and NavigationRail. Use Scaffold’s PaddingValues parameter to inset your critical UI. In most cases, that’s all you need to do.

However, there are cases where applying Scaffold’s PaddingValues will cause unexpected results. Scaffold’s PaddingValues includes insets for the top, bottom, start and end edges of the screen. You may need values for only certain edges. One approach is to make a copy of the parameter and manually adjust top, bottom, start and end insets so as to not apply too much padding.

Figure 14. Left: The input field at the bottom is obscured by the system’s navigation bar after targeting SDK 35. Middle: Scaffold’s PaddingValues applied to the input field. The system uses the size of the status bar and the top app bar to calculate the top padding value, which creates excess padding above the input field. Right: Scaffold’s PaddingValues applied but with top padding manually removed.

Here’s the incorrect code, causing the excess padding seen in the middle image of Figure 14.

// Causes excess padding, seen in the middle image of Figure 14.
Scaffold { innerPadding -> // innerPadding is Scaffold's PaddingValues
InputBar(
...
contentPadding = innerPadding
) {...}
}

Here’s the corrected code that generates proper padding, as seen in the right-side image of Figure 14.

// Function to make a copy of PaddingValues, using existing defaults unless an
// alternative value is specified
​​private fun PaddingValues.copy(
layoutDirection: LayoutDirection,
start: Dp? = null,
top: Dp? = null,
end: Dp? = null,
bottom: Dp? = null,
) = PaddingValues(
start = start ?: calculateStartPadding(layoutDirection),
top = top ?: calculateTopPadding(),
end = end ?: calculateEndPadding(layoutDirection),
bottom = bottom ?: calculateBottomPadding(),
)

// Produces correct padding, seen in the right-side image of Figure 14.
Scaffold { innerPadding -> // innerPadding is Scaffold's PaddingValues
val layoutDirection = LocalLayoutDirection.current
InputBar(
...
contentPadding = innerPadding.copy(layoutDirection, top = 0.dp)
) {...}
}

9. Use high level WindowInsets APIs

Similar to Scaffold’s PaddingValues, you can also use the high-level WindowInset APIs to easily and safely draw critical UI elements. These are:

See Inset fundamentals to learn more.

The following apply only for Views-based apps.

10. Prefer ViewCompat.setOnApplyWindowInsetsListener over fitsSystemWindows=true

You could use fitsSystemWindows=true to inset your app’s content. It’s an easy 1-line code change. However, don’t use fitsSystemWindows on a View that contains your entire layout (including the background). This will make your app look not edge-to-edge because fitsSystemWindows handles insets on all edges.

Figure 15. Edge-to-edge enforced. Left: fitsSystemWindows=false (the default). Right: fitsSystemWindows=true. This is not the result we want because the app does not look edge-to-edge.
Figure 16. Edge-to-edge enforced, fitsSystemWindows=true. The gap on the left edge is due to a display cutout, which is not visible here. This is not the result we want because the app doesn’t look edge-to-edge.

fitsSystemWindows can create an edge-to-edge experience if using CoordinatorLayouts or AppBarLayouts.Add fitsSystemWindows to the CoordinatorLayout and the AppBarLayout, and the AppBarLayout draws edge-to-edge, which is what we want.

Figure 17. Edge-to-edge enforced. Left: AppBarLayout does not automatically handle insets. Right: Add fitsSystemWindows to AppBarLayout and CoordinatorLayout to draw edge-to-edge.
<!-- Figure 17 -->
<CoordinatorLayout
android:fitsSystemWindows="true"
...>
<AppBarLayout
android:fitsSystemWindows="true"
...>
<TextView
android:text="App Bar Layout"
.../>
</AppBarLayout>
</CoordinatorLayout>

In this case, AppBarLayout used fitsSystemWindows to draw underneath the status bar rather than avoiding it, which is the opposite of what we might expect. Furthermore, AppBarLayout with fitsSystemWindows=true only applies padding for the top and not the bottom, start, or end edges.

The CoordinatorLayout and AppBarLayout objects have the following behavior when overriding fitsSystemWindows:

  • CoordinatorLayout: backgrounds of child views draw underneath the system bars if those views also set fitsSystemWindows=true. Padding is automatically applied to the content of those Views (e.g. text, icons, images) to account for system bars and display cutouts.
  • AppBarLayout: draws underneath the system bars if fitsSystemWindows=true and automatically applies top padding to content.

In most cases, handle insets with ViewCompat.setOnApplyWindowInsetsListener because it allows you to define which edges should handle insets and has consistent behavior. See tips #4 and #11 for a code example.

11. Apply insets based on app bar height during the layout phase

If you find that your app’s content is hiding underneath an app bar, you might need to apply insets after the app bar is laid out, taking the app bar height into account.

For example, if you have scrolling content underneath an AppBarLayout in a FrameLayout, you could use code like this to ensure the scrolling content appears after the AppBarLayout. Notice padding is applied within doOnLayout.

val myScrollView = findViewById<NestedScrollView>(R.id.my_scroll_view)
val myAppBar = findViewById<AppBarLayout>(R.id.my_app_bar_layout)

ViewCompat.setOnApplyWindowInsetsListener(myScrollView) { scrollView, windowInsets ->
val insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
)
myAppBar.doOnLayout { appBar ->
scrollView.updatePadding(
left = insets.left,
right = insets.right,
top = appBar.height,
bottom = insets.bottom
)
}
WindowInsetsCompat.CONSUMED
}

Likewise, if you have scrolling content that should sit above a BottomNavigationView, you’ll want to account for the BottomNavigationView’s height once it is laid out.

It might take significant work to properly support an edge-to-edge experience. Before you target SDK 35, consider how long you need to make the necessary changes in your app.

If you need more time to handle insets to be compatible with the system’s default edge-to-edge behavior, you can temporarily opt-out using R.attr#windowOptOutEdgeToEdgeEnforcement. But do not plan to use this flag indefinitely as it will be non-functional in the near future.

The flag might be particularly helpful for apps that have tens to hundreds of Activities. You might opt-out each Activity, then — make your app edge-to-edge one Activity at a time.

Here’s one approach to using this flag. Assuming your minSDK is less than 35, this attribute must be in values-v35.xml.

<!-- In values-v35.xml -->
<resources>
<!-- TODO: Remove once activities handle insets. -->
<style name="OptOutEdgeToEdgeEnforcement">
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>

Create an empty style for past versions in values.xml:

<!-- In values.xml -->
<resources>
<!-- TODO: Remove once activities handle insets. -->
<style name="OptOutEdgeToEdgeEnforcement">
<!-- android:windowOptOutEdgeToEdgeEnforcement
isn't supported before SDK 35. This empty
style enables programmatically opting-out. -->
</style>
</resources>

Call the style before accessing the decor view in setContentView:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

// Call before the DecorView is accessed in setContentView
theme.applyStyle(R.style.OptOutEdgeToEdgeEnforcement, /* force */ false)

super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
}
}

Android 15 AOSP released today. Our team has created blogs, videos, and codelabs to help get your app ready to handle the Android 15 edge-to-edge enforcement. What follows is a list of old and new resources for further learning.

Documentation

Handle edge-to-edge enforcements in Android 15 (Compose)



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *