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

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

5 takeaways from the Do iOS conference that will benefit our clients
iOS

5 takeaways from the Do iOS conference that will benefit our clients

November 30, 2023

Read more
What to know about the cost of custom app development
Business Process

What to know about the cost of custom app development

January 10, 2024

We hear a lot of ideas for apps at MichiganLabs. People from large enterprises and small startups, located all over the world, call us to explore their mobile and web-based application ideas, and one of the first questions they ask is: How much is this app going to cost?

Read more
The Pareto Principle at work in software
Business Process

The Pareto Principle at work in software

December 4, 2023

Read more
View more articles