Why Draw Behind The Status Bar?
🎵 I want it all,
I want it all,
I want it all,
and I want it now 🎵
It’s been 5 years since API 21 debuted, and with it, the ability to draw behind system windows like the status bar. It’s quite opportune too, with the ubiquity of the *sigh* notch on phones everywhere, and OEMs insistence that we embrace it.
As phones today all clamor to give us the perfect screen to body ratio, it’s only fair that we reward our users who spend their hard earned cash on the hardware feature du jour with a UI / UX that is worthy of their indulgence. Drawing behind the status gives us a great opportunity to do just that; a way to take advantage of the larger high resolution screens and make our apps really shine. It’s a path to offering a level of immersion that makes it seem that the device we’re using it on is really just all screen.
Insetting on WindowInsets
🎵 It’s a kind of magic, it’s a kind of magic…🎵
Insets are part of the UI that apps don’t draw in by default, and typically contain the System UI. This includes the top status bar, and onscreen navigation buttons at the bottom of most phones.
The size of these insets vary from device to device, and fortunately Android offers an API to figure out what they are. The easiest way to consume an inset for a s the
fitsSystemWindows xml attribute, commonly used in the
DrawerLayout as Ian Lake explains beautifully here. With that attribute,
Views use the
setOnApplyWindowInsetsListener method, and resize themselves to fit the insets when the insets are measured.
However, it may be a bit tedious to listen for each inset on each
View in the layout hierarchy that is interested in them, and in some cases interest in insets is not endemic to a
View; some navigation destinations may want to draw behind the status bar, and others may not. It’s therefore important to be able to take full control of the application of system insets, and apply them at our discretion.
🎵 Don’t stop me now, cuz I’m having a good time, having a good time…🎵
The first thing we need to do is tell the Android System we want to manage system insets ourselves. This is no small undertaking, as the Android System will honor that request entirely. This means we’re completely on our own for all UI positioning for all system inset changes, especially for the soft input keyboard. Flags for
adjustPan will no longer have any effect in our activity, and we’ll need to listen for the appearance of the keyboard and manually adjust our views ourselves.
It also means taking responsibility for Transient
Snackbars who internally listen to insets themselves, and overriding their default inset behavior. It’s a lot, but you’ve read this far, so let’s do this!
🎵 And you’re rushing headlong you’ve got a new goal
And you’re rushing headlong, out of control
And you think you’re so strong
But there ain’t no stopping and there’s nothin’
You can do about it 🎵
The first thing we need to do is tell the system we intend to take up some insets with our app. To do this, we obtain the hosting
DecorView and add the requisite flags. In our particular case, we only care about drawing behind the status bar so we use:
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
The important bit mask in the flag is
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN which tells the system we intend to draw behind the status bar, but we don’t want the status bar to be hidden. The are various permutations of this flag for the UI / UX you’re trying to achieve, a fragment displaying fullscreen video may add the following for example:
Which says to layout the screen to draw behind the status AND navigation bars, as well as hide them so the app fully takes up the entire screen. Detailed descriptions of each flag can be read at the Android Developer site here.
Application of the flag above yields:
We’re now drawing behind the status bar! However our
Toolbar is riding too high, it looks mashed together and a tad amateurish. How do we fix this? Remember the
setOnApplyWindowInsetsListener from before? We’re going to use it to consume the system insets so we know just how much to move our views to get everything picture perfect.
We can be clever about this though. In our case we want the list of dogs to be normal, only drawing behind the status bar in the detail and adoption views for each dog. Therefore it makes sense that each fragment describe its own
InsetState, outlining what parts of it should be inset or not. To do that, we can add a
FragmentManager.FragmentLifecycleCallbacks to listen for when each fragment is visible in our hosting
Activity, and then adjust insets appropriately.
We also don’t want to have custom inset behavior for each fragment, we want a generic approach to take care of each fragment all the time, and to do that, we rely on our trusty old
🎵 I’m the invisible man,
I’m the invisible man,
Incredible how you can,
See right through me 🎵
An easy way to do this is section off our screen into 3 vertically stacked parts:
- The invisible top inset view for the status bar (Dynamic)
- The actual content of our fragment (Static)
- The invisible padding view for the software keyboard (Dynamic)
With this layout sandwich defined, we can watch for
Fragments to show up in the
Activity, and animate the expansion of the top inset view to match the status bar height, and animate it away when we’re done, and do the same same for the software keyboard.
First lets find define the skeleton for our activity that watches for insets and adjusts itself for it’s child fragments with the following:
The code above does the following:
- Register our
FragmentManager.FragmentLifecycleCallbackson Activity creation to adjust insets for each fragment as it appears.
- Tell the activity we intend to draw behind the status bar and listen for the size of the
setContentView. This gives us the basic insets we need to measure once per configuration change.
- Upon receiving our system insets in
consumeSystemInsets, adjust the visibility of the inset views according to the currently displayed fragment. We use the
TransitionManagerto animate the shrinking and expanding to smoothen the transition.
- Repeat “3” each time a fragment comes into the fragment container.
When the above runs, we have our fragment’s content properly positioned within the container. In the case for the
AdoptDoggoFragment where we specify it has no top inset, we get the following:
Interacting with the input fields will also move the content view of the
Fragment, similar to the
adjustResize in a regular
The sample code makes use of a
Snackbar, but didn’t have to modify the behavior of a the
Snackbar with regards to insets because we didn’t ask to hide the navigation buttons, we would’ve had to otherwise. Again, with great power, comes great responsibility.
🎵 Another one bites the dust
Another one bites the dust
And another one gone, and another one gone
Another one bites the dust
Hey, I’m gonna get you, too
Another one bites the dust 🎵
Our final result looks like this:
If you had a notch, you’d get the same result, except well, with the intrusion of a notch 🙃.
Looks like we’ve helped our doggos find new homes 😊.
Full source for the app sample can be found below and thanks again Lola for helping me edit.