In Android, the screen typically consists of both the app content and system-drawn components, including the top status bar and the bottom navigation bar. By default, these bars are fully managed and rendered by the system, which automatically sets their backgrounds according to the current theme, leaving the remaining area for the app to draw its content.
In many cases, we want the application’s drawing area to extend into the System UI or dynamically set its colors to achieve a better user experience.
Years ago, implementing an immersive status bar required setting a bunch of flags, XML configurations, etc. However, times have changed, and AndroidX now provides more convenient tools. This article will introduce how to achieve an immersive effect using Jetpack Compose.
To implement an immersive UI, we need to follow two steps:
- Extend the application’s drawing area into the System UI portion.
- Set the background color of the System UI and the margins of the page as needed.
enableEdgeToEdge()
By default, the background of the System UI area is drawn by the system with color blocks, as shown in the figure below.
To address this issue, you need to call the enableEdgeToEdge()
function within Activity.onCreate
. This function extends the application’s layout behind the System UI, allowing us to control the drawing of the System UI background. In most cases, we prefer a solid color block, but sometimes we might want it to be transparent.
At this point, the page will look like the image below, showing that the application’s drawing area has extended into the System UI portion.
Setting the System UI Background and Margins After completing the above steps, we still need to address another issue: the page margins. At this point, the content of the application might be obscured by the System UI, so we need to add appropriate margins to the page.
WindowInsets
WindowInsets
is used to represent the position and size of the System UI.
@Stable
interface WindowInsets {
/**
* The space, in pixels, at the left of the window that the inset represents.
*/
fun getLeft(density: Density, layoutDirection: LayoutDirection): Int
/**
* The space, in pixels, at the top of the window that the inset represents.
*/
fun getTop(density: Density): Int
/**
* The space, in pixels, at the right of the window that the inset represents.
*/
fun getRight(density: Density, layoutDirection: LayoutDirection): Int
/**
* The space, in pixels, at the bottom of the window that the inset represents.
*/
fun getBottom(density: Density): Int
companion object
}
We set the page margins by obtaining the WindowInsets
object corresponding to the System UI.
WindowInsets
contains various types, each corresponding to different types of System UI.
// The insets describing the status bars. These are the top system UI bars containing notification icons and other indicators.
WindowInsets.statusBars
// The status bar insets for when they are visible. If the status bars are currently hidden (due to entering immersive full screen mode), then the main status bar insets will be empty, but these insets will be non-empty.
WindowInsets.statusBarsIgnoringVisibility
// The insets describing the navigation bars. These are the system UI bars on the left, right, or bottom side of the device, describing the taskbar or navigation icons. These can change at runtime based on the user's preferred navigation method and interacting with the taskbar.
WindowInsets.navigationBars
// The navigation bar insets for when they are visible. If the navigation bars are currently hidden (due to entering immersive full screen mode), then the main navigation bar insets will be empty, but these insets will be non-empty.
WindowInsets.navigationBarsIgnoringVisibility
// ... and more ...
Page Padding
In the example above, our page currently extends into the System UI, causing part of the content to be obscured.
Now, we can obtain the padding through WindowInsets
and apply it.
val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.getTop(density).pxToDp(density)
val navigatorBarHeight = WindowInsets.navigationBars.getBottom(density).pxToDp(density)
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = statusBarHeight, bottom = navigatorBarHeight),
)
Then, the page will look like this:
Job Offers
Doesn’t it look much better now?
Adaptive Padding in Composables
However, setting up each page like this seems cumbersome, doesn’t it? Fortunately, Compose provides many handy tools to assist developers in setting up margins.
For example, in the above example, you can also do this:
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding(),
)
// or Modifier.systemBarsPadding()
// or Modifier.safeDrawingPadding()
The above code ultimately prevents content from being obscured by adding padding.
But it’s still a bit cumbersome. Do we have to write like this for every page? Of course not. Compose’s Scaffold
also helps us solve this problem.
When using the Material Design 3’s Scaffold
, correctly using innerPadding
helps us automatically add page margins.
Scaffold { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
// ...
}
}
Here, innerPadding
already includes the padding of the status bar and navigation bar, so you can apply it directly.
Furthermore, the behavior of Scaffold
can be changed. If our page doesn’t need to add margins (such as an image viewer or video player), it’s better not to use Scaffold
‘s innerPadding
, but to control it through parameters.
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
)
As you can see, the Scaffold
parameter includes a contentWindowInsets
parameter, which by default adds the System UI’s WindowInsets
. If you want to change this behavior, you can set an empty WindowInsets
.
TopAppBar
When using Scaffold
, the adaptive margin is achieved by applying innerPadding
. However, Scaffold
‘s topBar
does not have innerPadding
, but we still don’t need to manually set its padding. This is because, as a special component like TopAppBar
, its internals also automatically set the padding.
@Composable
fun TopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
)
The windowInsets
parameter in the above parameters is used to set the top padding. We can also change this behavior through parameters.
NavigationBar
As the NavigationBar at the bottom of the page, it also supports adaptive page padding.
fun NavigationBar(
modifier: Modifier = Modifier,
containerColor: Color = NavigationBarDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = NavigationBarDefaults.Elevation,
windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
content: @Composable RowScope.() -> Unit
)
StatusBar and NavigationBar Background Colors
In fact, after the above steps are set, the background color is easy to set, because this is all part of the page content. After all, it’s your code, so you can control it however you want.
For Compose, this is automatically set, too. If our page uses Scaffold
, then Scaffold
itself has a background color, which is the backgroundColor
in Material Design.
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
)
The containerColor
in the above code is the background color of the page. And because Scaffold
adds padding to the top and bottom by default, the background color of StatusBar/NavigationBar is naturally the background color of the page.
If you use TopAppBar
, it will also automatically set the color, and it will change according to the scrolling state. So if the page has a top bar, it’s best to use the official one because it looks much nicer, and it also saves us a lot of trouble.
Conclusion
In short, setting up an immersive status bar according to the latest specifications will be very convenient, and many components in Compose help us save a lot of work. Even if you encounter some special cases, there are concise ways to deal with them.
Also, feel free to follow me. I’ll continue to produce various types of original content.
Reference Documentation:
https://developer.android.com/develop/ui/views/layout/edge-to-edge
https://developer.android.google.cn/develop/ui/compose/layouts/insets
This article is previously published on proandroiddev.com.