Modifying Android's Navigation Bar for a More Immersive Experience
March 20, 2018A majority of Android devices have a software navigation bar which (for every other app I can think of) is a solid black (or white) bar that takes up 48dp of your vertical screen space. On the Google Pixel this equates to somewhere between 7 – 10% of the total vertical screen space (depending on your accessibility settings for screen size). Considering that most mobile apps are optimized for showing you a list of things in a window that scrolls vertically, we might want to reclaim some of this space for the content of our apps. This will make your app feel more immersive by using more of the screen to display your content. You could certainly hide the navigation UI altogether, but this makes it more difficult for the user to navigate between screens in your app because they will have to swipe up from the bottom of the screen to reveal the hidden navigation bar. I stumbled on a better answer by accident when I noticed that the news feed section of the Pixel launcher has a semi-transparent bottom navigation bar:
As it turns out, this translucent navbar option has been around since KitKat (API Level 19)! All you have to do is set android:windowTranslucentNavigation
to true
and it just works, right?
The Part Where it Gets Complicated #
Nope, sorry, I lied. Even though we’ve requested that the app be shown with a translucent navigation bar, the app’s window still is sized to the area between the navigation bar and the statusbar. Fortunately, the KitKat release notes helpfully explain that in order to get your app to display content under the navigation bar, you must also set android:fitsSystemWindows
to false
. This allows the Activity’s window to be drawn at the full size of the device’s screen.
By this point, you’ve probably wondered “How do I tap on items that are displayed underneath the navigation bar?” The answer is “You can’t” because the navigation bar does not pass touch events through to your application. Therefore, we have to add padding to the bottom of the app’s content so that it can be scrolled into an accessible position above the navigation bar. The amount of padding required is simply the height of the navbar which be determined by retrieving the value of android.R.dimen.navigation_bar_height
from the system 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 pixels of padding to the bottom of your layout’s scrolling element (in this example, 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
otherwise Android will not draw any child views inside the padding area.
What About Hardware Navigation Buttons? #
Phones such as the Samsung Galaxy S7 use physical buttons for the Back/Home/Switcher actions and do not show the software navigation bar on the screen. Some of these even let you choose between the hardware buttons and the onscreen buttons. Fortunately, we can read android.R.bool.config_showNavigationBar
to determine if the software 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(/* ... */)
}
Everything Goes Sideways #
Rotation configuration changes have always been tricky for Android apps to handle and this is no different. When you rotate a phone into the 90° or 270° orientation, the navigation bar remains on the side of the phone that is the “bottom” in the 0° orientation. In these orientations, displaying content under the navbar loses its usefulness, so we’ll disable it. The easiest way to do this is by overriding these attributes 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 content 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 bottom of the screen in all orientations. There is no isTablet
property provided by Android since the definition of “tablet” is rather ambiguous in the presence of large “phablet” phones, so we have to create our own. Android treats devices with a screen whose smallest dimension is at least 600dp differently, most importantly for our use case, letting the navigation bar rotate. We can again use resource splits to create different values for isTablet
depending 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) introduced yet another element for us to worry about: Mutli-Window mode. In Multi-Window mode, two activities may be shown side-by-side or stacked vertically depending on device orientation and neither app gets to draw under the navigation bar. Since we cannot perform any of our fancy navigation bar styling in these cases, we will disable our modifications to the activity theme and padding applied to the content in Multi-Window 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 Activity is also drawn underneath the statusbar, which we have completely ignored so far. Fortunately, its behavior is much more predictable than the navigation bar since it is always shown on all phones (except when explicitly hidden by the activity 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 follow the fitSystemWindows
setting 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 Candy #
Right at the beginning, I said that KitKat was the Android version that enabled use of translucent navigation bars, but like so many things in KitKat, you can’t get it to work properly. The config_showNavigationBar
value that we check to see if we need to inset the scrolling content always returns false
when fitsSystemWindows
is false
. The navigation_bar_height
dimension also always returns 0 in this case, making it impossible to measure how much padding we should add to the content. To implement our solution on KitKat we have to assume the device always has a software navigation bar (which is less and less likely the older the device is), estimate the padding required (almost always 48dp, 56dp for tablets meeting the sw900dp
qualifier), and just deal with excess space at the bottom for devices that don’t. That’s too many conditions to leave to chance (especially for KitKat), so I’ve opted to disable the translucent navbar using another 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>
Conclusion #
There’s a lot of stuff to keep in mind here, so I’ve made a sample app demonstrating all these concepts available on GitHub. If you’d like to play around with the app, it is also available on the Google Play Store. It’s a very simple example that shows the colors of the Material palette in a grid:
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
What to know about the cost of custom app development
January 10, 2024We 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