Android , Design , Development

Modifying Android's Navigation Bar for a More Immersive Experience

March 20, 2018

A major­i­ty of Android devices have a soft­ware nav­i­ga­tion bar which (for every oth­er app I can think of) is a sol­id black (or white) bar that takes up 48dp of your ver­ti­cal screen space. On the Google Pix­el this equates to some­where between 7 – 10% of the total ver­ti­cal screen space (depend­ing on your acces­si­bil­i­ty set­tings for screen size). Con­sid­er­ing that most mobile apps are opti­mized for show­ing you a list of things in a win­dow that scrolls ver­ti­cal­ly, we might want to reclaim some of this space for the con­tent of our apps. This will make your app feel more immer­sive by using more of the screen to dis­play your con­tent. You could cer­tain­ly hide the nav­i­ga­tion UI alto­geth­er, but this makes it more dif­fi­cult for the user to nav­i­gate between screens in your app because they will have to swipe up from the bot­tom of the screen to reveal the hid­den nav­i­ga­tion bar. I stum­bled on a bet­ter answer by acci­dent when I noticed that the news feed sec­tion of the Pix­el launch­er has a semi-trans­par­ent bot­tom nav­i­ga­tion bar:

Google Feed

As it turns out, this translu­cent navbar option has been around since KitKat (API Lev­el 19)! All you have to do is set android:windowTranslucentNavigation to true and it just works, right?

The Part Where it Gets Com­pli­cat­ed #

Nope, sor­ry, I lied. Even though we’ve request­ed that the app be shown with a translu­cent nav­i­ga­tion bar, the app’s win­dow still is sized to the area between the nav­i­ga­tion bar and the sta­tus­bar. For­tu­nate­ly, the KitKat release notes help­ful­ly explain that in order to get your app to dis­play con­tent under the nav­i­ga­tion bar, you must also set android:fitsSystemWindows to false. This allows the Activity’s win­dow to be drawn at the full size of the device’s screen.

By this point, you’ve prob­a­bly won­dered How do I tap on items that are dis­played under­neath the nav­i­ga­tion bar?” The answer is You can’t” because the nav­i­ga­tion bar does not pass touch events through to your appli­ca­tion. There­fore, we have to add padding to the bot­tom of the app’s con­tent so that it can be scrolled into an acces­si­ble posi­tion above the nav­i­ga­tion bar. The amount of padding required is sim­ply the height of the navbar which be deter­mined by retriev­ing the val­ue of android.R.dimen.navigation_bar_height from the sys­tem resources:

val Resources.navBarHeight: Int @Px get() {
  val id = getIdentifier("navigation_bar_height", "dimen", "android")
  return when {
    id > 0 -> getDimensionPixelSize(id)
    else -> 0
  }
}

Then apply that many pix­els of padding to the bot­tom of your layout’s scrolling ele­ment (in this exam­ple, a RecyclerView):

recyclerView.setPaddingRelative(
  recyclerView.paddingStart,
  recyclerView.paddingTop,
  recyclerView.paddingEnd,
  recyclerView.paddingBottom + resources.navBarHeight
)

You will also need to set android:clipToPadding to false on your RecyclerView or ScrollView oth­er­wise Android will not draw any child views inside the padding area.

What About Hard­ware Nav­i­ga­tion But­tons? #

Phones such as the Sam­sung Galaxy S7 use phys­i­cal but­tons for the Back/​Home/​Switcher actions and do not show the soft­ware nav­i­ga­tion bar on the screen. Some of these even let you choose between the hard­ware but­tons and the onscreen but­tons. For­tu­nate­ly, we can read android.R.bool.config_showNavigationBar to deter­mine if the soft­ware navbar is being shown:

val Resources.showsSoftwareNavBar: Boolean get() {
  val id = getIdentifier("config_showNavigationBar", "bool", "android")
  return id > 0 && getBoolean(id)
}

// Inset bottom of content if drawing under the translucent navbar, but
// only if the navbar is a software bar
if (resources.showsSoftwareNavBar) {
  recyclerView.setPaddingRelative(/* ... */)
}

Every­thing Goes Side­ways #

Rota­tion con­fig­u­ra­tion changes have always been tricky for Android apps to han­dle and this is no dif­fer­ent. When you rotate a phone into the 90° or 270° ori­en­ta­tion, the nav­i­ga­tion bar remains on the side of the phone that is the bot­tom” in the 0° ori­en­ta­tion. In these ori­en­ta­tions, dis­play­ing con­tent under the navbar los­es its use­ful­ness, so we’ll dis­able it. The eas­i­est way to do this is by over­rid­ing these attrib­ut­es of the activity’s theme based on orientation:

<!-- values/styles.xml -->
<bool name="fullscreen_style_fit_system_windows">false</bool>
<bool name="fullscreen_style_use_translucent_nav">true</bool>
<style name="AppTheme.Fullscreen">
    <item name="android:fitsSystemWindows">
        @bool/fullscreen_style_fit_system_windows
    </item>
    <item name="android:windowTranslucentNavigation">
        @bool/fullscreen_style_use_translucent_nav
    </item>
</style>

<!-- values-land/styles.xml -->
<bool name="fullscreen_style_fit_system_windows">true</bool>
<bool name="fullscreen_style_use_translucent_nav">false</bool>

We will also have to add the padding to the scrolling con­tent of the view conditionally:

inline val Resources.isNavBarAtBottom: Boolean get() {
  // Navbar is always on the bottom of the screen in portrait mode, but rotates
  // with device in landscape orientations
  return this.configuration.orientation == ORIENTATION_PORTRAIT
}

// Inset bottom of content if drawing under the translucent navbar, but
// only if the navbar is a software bar and is on the bottom of the screen.
if (resources.showsSoftwareNavBar && resources.isNavBarAtBottom) {
  recyclerView.setPaddingRelative(/* ... */)
}

But wait! What about tablets? Tablets do rotate the navbar to the bot­tom of the screen in all ori­en­ta­tions. There is no isTablet prop­er­ty pro­vid­ed by Android since the def­i­n­i­tion of tablet” is rather ambigu­ous in the pres­ence of large phablet” phones, so we have to cre­ate our own. Android treats devices with a screen whose small­est dimen­sion is at least 600dp dif­fer­ent­ly, most impor­tant­ly for our use case, let­ting the nav­i­ga­tion bar rotate. We can again use resource splits to cre­ate dif­fer­ent val­ues for isTablet depend­ing on the device’s screen that we can check in our code:

<!-- values/bools.xml -->
<bool name="is_tablet">false</bool>

<!-- values-sw600dp/bools.xml -->
<bool name="is_tablet">true</bool>
inline val Resources.isTablet: Boolean get() = getBoolean(R.bool.is_tablet)

inline val Resources.isNavBarAtBottom: Boolean get() {
  // Navbar is always on the bottom of the screen in portrait mode, but may
  // rotate with device if its category is sw600dp or above.
  return this.isTablet || this.configuration.orientation == ORIENTATION_PORTRAIT
}

Catch 24 #

Android 7.0 Nougat (API 24) intro­duced yet anoth­er ele­ment for us to wor­ry about: Mut­li-Win­dow mode. In Mul­ti-Win­dow mode, two activ­i­ties may be shown side-by-side or stacked ver­ti­cal­ly depend­ing on device ori­en­ta­tion and nei­ther app gets to draw under the nav­i­ga­tion bar. Since we can­not per­form any of our fan­cy nav­i­ga­tion bar styling in these cas­es, we will dis­able our mod­i­fi­ca­tions to the activ­i­ty theme and padding applied to the con­tent in Mul­ti-Win­dow mode:

inline val Activity.isInMultiWindow: Boolean get() {
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    isInMultiWindowMode
  } else {
    false
  }
}

// Apps can't draw under the navbar in multiwindow mode.
val fitSystemWindows = if (activity?.isInMultiWindow == true) {
  true
} else {
  resources.getBoolean(R.bool.fullscreen_style_fit_system_windows)
}
// Override the activity's theme when in multiwindow mode.
coordinatorLayout.fitsSystemWindows = fitSystemWindows

if (!fitSystemWindows) {
  // Inset bottom of content if drawing under the translucent navbar, but
  // only if the navbar is a software bar and is on the bottom of the screen.
  if (resources.showsSoftwareNavBar && resources.isNavBarAtBottom) {
    recyclerView.setPaddingRelative(/* ... */)
  }
}

One More Thing #

With fitsSystemWindows="false", the Activ­i­ty is also drawn under­neath the sta­tus­bar, which we have com­plete­ly ignored so far. For­tu­nate­ly, its behav­ior is much more pre­dictable than the nav­i­ga­tion bar since it is always shown on all phones (except when explic­it­ly hid­den by the activ­i­ty theme, which we did not do), and it is always shown at the top of the screen. Thus, whether we add padding to our AppBarLayout or not will fol­low the fitSystemWindows set­ting with no exceptions:

val Resources.statusBarHeight: Int @Px get() {
  val id = getIdentifier("status_bar_height", "dimen", "android")
  return when {
    id > 0 -> getDimensionPixelSize(id)
    else -> 0
  }
}

if (!fitSystemWindows) {
  // ...
  // Inset the toolbar when it is drawn under the status bar.
  barLayout.updatePaddingRelative(
    barLayout.paddingStart,
    barLayout.paddingTop + resources.statusBarHeight,
    barLayout.paddingEnd,
    barLayout.paddingBottom
  )
}

A Piece of Everyone’s Least Favorite Can­dy #

Right at the begin­ning, I said that KitKat was the Android ver­sion that enabled use of translu­cent nav­i­ga­tion bars, but like so many things in KitKat, you can’t get it to work prop­er­ly. The config_showNavigationBar val­ue that we check to see if we need to inset the scrolling con­tent always returns false when fitsSystemWindows is false. The navigation_bar_height dimen­sion also always returns 0 in this case, mak­ing it impos­si­ble to mea­sure how much padding we should add to the con­tent. To imple­ment our solu­tion on KitKat we have to assume the device always has a soft­ware nav­i­ga­tion bar (which is less and less like­ly the old­er the device is), esti­mate the padding required (almost always 48dp, 56dp for tablets meet­ing the sw900dp qual­i­fi­er), and just deal with excess space at the bot­tom for devices that don’t. That’s too many con­di­tions to leave to chance (espe­cial­ly for KitKat), so I’ve opt­ed to dis­able the translu­cent navbar using anoth­er resource split:

<!-- values/styles.xml -->
<bool name="fullscreen_style_fit_system_windows">true</bool>
<bool name="fullscreen_style_use_translucent_nav">true</bool>
<style name="AppTheme.Fullscreen">
  <!-- KitKat can show a translucent navbar, but config_showNavigationBar
    is always false so you can't really tell if the device has hardware nav
    keys or not. -->
  <item name="android:fitsSystemWindows">
    @bool/fullscreen_style_fit_system_windows
  </item>
</style>

<!-- values-v21/styles.xml -->
<bool name="fullscreen_style_fit_system_windows">false</bool>
<style name="AppTheme.Fullscreen">
  <item name="android:fitsSystemWindows">
    @bool/fullscreen_style_fit_system_windows
  </item>
  <item name="android:windowTranslucentNavigation">
    @bool/fullscreen_style_use_translucent_nav
  </item>
</style>

Con­clu­sion #

There’s a lot of stuff to keep in mind here, so I’ve made a sam­ple app demon­strat­ing all these con­cepts avail­able on GitHub. If you’d like to play around with the app, it is also avail­able on the Google Play Store. It’s a very sim­ple exam­ple that shows the col­ors of the Mate­r­i­al palette in a grid:

Translucent Navbar

Ready to get started?

Call us at 616-594-0269 or send us a note below.
Visit our office @ 452 Ada Drive SE Suite 300 Ada, Michigan 49301
Send us an e-mail @ info@michiganlabs.com