Gesture Navigation: handling visual overlaps (II)
This is the second post in our series on Gesture Navigation. If you want to skip to another post, you can find them listed below:
Gesture Navigation: Going edge-to-edge (I)
With Android Q, a new system navigation mode has been added, allowing the user to navigate back, and to the home screen…
Gesture Navigation: Handling gesture conflicts (III)
This is the third post in our blog post series on we covered how to handle visual overlaps after updating an app to…
In part 1 of this series, we explored how you can make your app go ‘edge-to-edge’. Unfortunately this can result in some of your views being drawn behind the system bars, obscuring them from the user. This post explores how we can inset those views, moving them away from the system bars.
For the rest of this post I’ll be referring to something called the ‘system UI’. This is what we call any of the system provided UI on screen, such as the navigation bar and status bar. It also includes things like the notifications panel.
The term insets tends to strike fear into Android developers, usually from their experience trying to draw behind the status bar in the days of Android Lollipop. For example, this very old StackOverflow question on the topic has a lot of views 😲.
Insets tell you which parts of the screen intersect with the system UI, like the navigation or status bar. Intersecting could mean simply being displayed above your content, but it can also tell you about system gestures too. We can use the insets to try and remove any conflicts, for example by moving a view in from the edges.
On Android, insets are represented by the
WindowInsets class, and
WindowInsetsCompat in AndroidX. With Android Q, we have 5 types of insets to consider when laying out your app. Which inset type you use depends on the situation you’re in, so let’s go through each type and look…
System window insets
System window insets are the most common type of insets in use today. They have been around since API 1 in various forms, and are dispatched to the view hierarchy whenever the system UI is displayed above your app (in z-axis). Common examples of this are the status bar and navigation bar, but also include the on-screen keyboard (IME).
Let’s look at an example where you would use the system window insets. Here we have a
FloatingActionButton (FAB), which is placed at the bottom corner of the screen, with a margin of
16dp (as per the guidelines).
Once we’ve completed steps 1 and 2 from the previous post, our views are now laid out to extend behind the navigation bar:
You can see that our conference schedule list is now extending behind the navigation bar, which is what we want to create a more immersive experience ✔️. We’ll go into more detail of how to handle lists/grids in a later post.
Back to the example, you can also see that the FAB is now being obscured, meaning the user may not be able to interact/click the view. This kind of visual conflict is something we want to avoid 🚫. The example is more obvious when the device is set to use the button navigation mode (pictured) since the bar is taller. In gesture navigation with dynamic color adaptation it does actually kinda work, but remember that the system can switch to a translucent scrim at any time, which could break any interaction.
This is a good time to point out that you should now be testing your app in all navigation modes.
So how do we handle the visual conflict? This is where system window insets come into play. They tell you where the system bars are being laid out above your view hierarchy, so we can use those values to move our views away from the system bars.
In the example above, the FAB is laid out near the bottom & right edges, therefore we can use the
systemWindowInsets.right values to increase the view’s margins on each dimension, to move it away from the navigation bar.
Once we’ve done that we get the following instead:
We will talk more about how exactly to implement this, later in this post.
TL;DR: System window insets work best for moving/padding views which are clickable, and should not be visually obscured by the system bars.
Tappable element insets
Next up are the new tappable element insets which are new in Android Q. These are very similar to the system window insets above, but respond to the varying visibility of the navigation bar.
TL;DR for ‘tappable element insets’: ignore them and use ‘system window insets’ instead. You can skip to ‘Gesture Insets’ below, or keep reading to know more. 🕵️
Tappable element insets define the minimum insets which should be applied to clickable (tappable) views. Minimum in this case means that the value can still result in conflicts with the system bars. This differs from the system window insets, which provide values which always avoid conflicts with the system bars.
Lets use our FloatingActionButton example to show the differences in the values:
Remember, never hardcode the values from the table above since the navigation bar can change size — use insets.
You can see that the ‘tappable element insets’ and ‘system gesture insets’ act the same when the device is set to button navigation. The key difference is when the device is set to gesture navigation and has color adaptation enabled. In this scenario, the navigation bar is transparent meaning that tappable views could theoretically be placed within it, which is why it contains a bottom value of 0.
Insets though have no concept of where views should be placed, so you could theoretically get something like this when using the tappable element insets:
This is not ideal since the view is extremely close to the navigation bar, making it confusing for the user.
In practice, nearly all usages of tappable element insets are better served by the ‘system window insets’ instead.
The next type of insets we will cover are the new system gesture insets, added to the platform in Android Q. To recap, Android Q bring a new gestural navigation mode, allowing the user to navigate their device via two touch gestures:
- Swipe horizontally from one of the display’s edges. This triggers the back functionality.
- Swiping up from the bottom display edge. This allows the user to go to their home screen or recent apps.
System gesture insets represent the areas of the window where the system gestures take priority over your app’s touch gestures. You might have noticed that I listed two methods above. This is because there are actually two types of system gesture insets: one containing all of the gesture areas, and then a subset containing the mandatory system gesture insets.
System gesture insets
First up we have the system gesture insets. These insets contain all of the areas on screen where the system gestures take priority over your apps gestures. On Android Q this means that the insets would look a bit like this, containing a bottom inset for the home gesture, and a left and right inset for the back gestures:
| System |
40 | Gesture | 40
| Insets |
When would you use the system gesture insets? These insets tell you where the system gestures take priority, so you can use them to proactively move any views which require swipe gestures to operate.
Mandatory system gesture insets
The mandatory system gestures are a subset of the system gesture insets, and contain only the areas which can not be excluded by apps (thus the name). We’ve skipped ahead a bit here to the next blog post, where we’ll be talking handling gesture conflicts, but for the purposes of this blog post just know that apps have the ability to exclude the system gestures for certain parts of the screen.
Mandatory system gesture insets tell you the areas of the screen where the system gestures always take priority, and are mandatory. On Android Q, the only area which is currently mandatory is the home gesture area at the bottom of the screen. This is so that the user can always exit an app.
If we take a look at an example of the gesture insets from an Android Q device, you might get something like this:
| | | Mandatory |
| System | | System |
40 | Gesture | 40 0 | Gesture | 0
| Insets | | Insets |
| | | |
You can see that the system gesture insets contain a left, right and bottom, whereas the mandatory only contain the bottom inset for the home gesture. We’ll talk more about excluding gesture areas in the next blog post.
Stable insets are the final type of inset available on Android. They are not particularly relevant to gesture navigation, but I figured we’re quickly cover them for completeness.
Stable insets are related to system window insets, but they denote where the system UI might be displayed over your app, opposed to where the system UI is displayed. Stable insets are primarily used when the system UI is set to a mode where its visibility can be toggled on/off, such as when using the lean back or immersive modes (common example: games, photo viewers, video players).
Hopefully you now have a better understanding of the different types of insets, so now let’s look at how you actually use them in your apps.
The primary method to access
WindowInsets is via the
setOnApplyWindowInsetsListener method. Let’s look at an example of a view where we want to add padding so that it is not displayed behind the navigation bar:
Here we are just setting the view’s bottom padding to whatever the system window inset bottom value is.
Note: if you’re doing this on a
ViewGroup, you probably want to set
android:clipToPadding="false"on it. This is because all views clip drawing for within padding by default. This is attribute is commonly used with
RecyclerView, which we’ll cover in more detail in a later post.
Make sure your listener function is idempotent though. If the listener is called multiple times with the same insets, the result should be the same each time. An example of not being idempotent is below:
🙅 You should not be increasing (ala
+=) a view’s padding each time the listener is called. A window insets pass can happen at any time, and multiple times during the life of the view hierarchy.
One thing to note for insets, I recommend to always use the
WindowInsetsCompat class from Jetpack, regardless of your minimum SDK version. The WindowInsets API has been improved and expanded over the years, and the compat version provides a consistent API and behavior across all API levels.
Where I’ve the new insets types available in Android Q, the compat method provides a set of values which are correct for the host device on all API levels. To get access to the new APIs in AndroidX, make sure to update to
androidx.core:core:1.2.0-xxx (currently in alpha) or higher. See here for the most recent version.
Going even further
The techniques I’ve mentioned above are the simplest way to use the
WindowInsets[Compat] APIs, but they can make your code very verbose and repetitive. I wrote a blog post earlier this year detailing some techniques to dramatically increase the ergonomics of the handling window insets using binding adapters. You can read more here:
In the next blog post in this series, we look at how to handle any gesture conflicts your app may have against the system gestures: