Adetunji Dahunsi

I Want it All, Owning The System Window and Consuming Insets

Why Draw Behind The Status Bar?

TJ Dahunsi

Oct 29 2018 · 7 min read

Categories:

Why Draw Behind The Status Bar?

Living it all, and wanting it all Living it all, and wanting it all

🎵 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…🎵

The StatusBar inset The StatusBar inset

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](https://developer.android.com/reference/android/view/View.html#setOnApplyWindowInsetsListener(android.view.View.OnApplyWindowInsetsListener)) 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.

Digging in

🎵 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 android:windowSoftInputMode like adjustResize and 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 Views like 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 Activity’s DecorView and add the requisite flags. In our particular case, we only care about drawing behind the status bar so we use:

1getWindow().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:

1 private void hideSystemUI() { 2 int visibility = SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 3 visibility = visibility | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | SYSTEM_UI_FLAG_FULLSCREEN; 4 if (isInLandscape()) visibility = visibility | SYSTEM_UI_FLAG_HIDE_NAVIGATION; 5 6 getDecorView().setSystemUiVisibility(visibility); 7 }

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 drawing behind the status bar! But something is off.. We’re drawing behind the status bar! But something is off..

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](https://developer.android.com/reference/android/view/View.html#setOnApplyWindowInsetsListener(android.view.View.OnApplyWindowInsetsListener)) 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 ConstraintLayout.

🎵 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:

  1. The invisible top inset view for the status bar (Dynamic)

  2. The actual content of our fragment (Static)

  3. The invisible padding view for the software keyboard (Dynamic)

Our layout sandwich Our layout sandwich

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:

1 2 final FragmentManager.FragmentLifecycleCallbacks fragmentViewCreatedCallback = new FragmentManager.FragmentLifecycleCallbacks() { 3 @Override 4 public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull androidx.fragment.app.Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) { 5 if (isNotInMainFragmentContainer(v)) return; 6 adjustInsetForFragment(f); 7 setOnApplyWindowInsetsListener(v, (view, insets) -> consumeFragmentInsets(insets)); 8 } 9 }; 10 11 public void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.transparent)); 14 getSupportFragmentManager().registerFragmentLifecycleCallbacks(fragmentViewCreatedCallback, false); 15 setContentView(R.layout.activity_main); 16 } 17 18 19 public void setContentView(@LayoutRes int layoutResID) { 20 super.setContentView(layoutResID); 21 (...) 22 getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 23 setOnApplyWindowInsetsListener(this.constraintLayout, (view, insets) -> consumeSystemInsets(insets)); 24 } 25 26 private WindowInsetsCompat consumeSystemInsets(WindowInsetsCompat insets) { 27 if (this.insetsApplied) return insets; 28 29 topInset = insets.getSystemWindowInsetTop(); 30 leftInset = insets.getSystemWindowInsetLeft(); 31 rightInset = insets.getSystemWindowInsetRight(); 32 bottomInset = insets.getSystemWindowInsetBottom(); 33 34 ViewUtil.getLayoutParams(this.topInsetView).height = topInset; 35 ViewUtil.getLayoutParams(this.bottomInsetView).height = bottomInset; 36 37 adjustInsetForFragment(getCurrentFragment()); 38 39 this.insetsApplied = true; 40 return insets; 41 } 42 43 private WindowInsetsCompat consumeFragmentInsets(WindowInsetsCompat insets) { 44 getLayoutParams(keyboardPadding).height = insets.getSystemWindowInsetBottom() - bottomInset; 45 return insets; 46 } 47 48 private void adjustInsetForFragment(Fragment fragment) { 49 if (!(fragment instanceof AppBaseFragment)) {return;} 50 51 InsetFlags insetFlags = ((AppBaseFragment) fragment).insetFlags(); 52 ViewUtil.getLayoutParams(toolbar).topMargin = insetFlags.hasTopInset() ? 0 : topInset; 53 TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition() 54 .excludeChildren(RecyclerView.class, true) 55 .excludeChildren(ViewPager.class, true) 56 .setDuration(ANIMATION_DURATION) 57 ); 58 59 topInsetView.setVisibility(insetFlags.hasTopInset() ? View.VISIBLE : View.GONE); 60 constraintLayout.setPadding(insetFlags.hasLeftInset() ? this.leftInset : 0, 0, insetFlags.hasRightInset() ? this.rightInset : 0, 0); 61 }

The code above does the following:

  1. Register our FragmentManager.FragmentLifecycleCallbacks on Activity creation to adjust insets for each fragment as it appears.

  2. Tell the activity we intend to draw behind the status bar and listen for the size of the Activity’s insets in setContentView. This gives us the basic insets we need to measure once per configuration change.

  3. Upon receiving our system insets in consumeSystemInsets, adjust the visibility of the inset views according to the currently displayed fragment. We use the TransitionManager to animate the shrinking and expanding to smoothen the transition.

  4. Repeat “3” each time a fragment comes into the fragment container.

Sandwiched Fragment with insets Sandwiched Fragment with insets

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:

Setting the gold standard for apps with a golden retriever. Apt. Setting the gold standard for apps with a golden retriever. Apt.

Interacting with the input fields will also move the content view of the Fragment, similar to the android:windowSoftInputMode of adjustResize in a regular Activity.

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.

That’s it!

QueenQueen

🎵 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:

Our fullscreen UI for adopting GOOD BOYES and GIRLES Our fullscreen UI for adopting GOOD BOYES and GIRLES

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.

0