Gesture Navigation Tutorial for Android
In this Android tutorial, you will learn how to add support for gesture navigation to your app, a feature that was added in Android 10. By Denis Buketa.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Gesture Navigation Tutorial for Android
20 mins
- Learning the Gestures
- Getting Started
- Seeing the Problem at Hand
- Making Your App Edge-to-Edge
- Changing System Bar Colors
- Requesting Fullscreen Layout
- Leveraging Insets
- Understanding System Window Insets
- Leveraging System Window Insets
- System Gesture Insets
- Leveraging System Gesture Insets
- Mandatory System Gesture Insets
- Handling Conflicting App Gestures
- Handling Landscape Orientation
- Testing 3-Button Navigation and Gestural Navigation
- Where to Go From Here?
Understanding System Window Insets
System window insets tell you where the system UI displays over your app. You can use these insets to move clickable views away from the system bars.
To consume system window insets, you have to implement the OnApplyWindowInsetsListener
interface. WindowInsets
provide regular visual insets for all system bars through getSystemWindowInsets()
.
Leveraging System Window Insets
Now that you know about system windows insets, you can leverage them to improve your Notes Overview screen. At the top of NotesOverviewActivity.kt, add these imports to the list of imports:
import android.view.ViewGroup
import androidx.core.view.updatePadding
Below requestToBeLayoutFullscreen()
, add the following code:
private fun adaptViewForInsets() {
// Prepare original top padding of the toolbar
val toolbarOriginalTopPadding = toolbar.paddingTop
// Prepare original bottom margin of the "Add Note" button
val addNoteButtonMarginLayoutParam =
addNoteButton.layoutParams as ViewGroup.MarginLayoutParams
val addNoteButtonOriginalBottomMargin =
addNoteButtonMarginLayoutParam.bottomMargin
}
This code prepares everything you need to update your views for the system window insets. It stores the toolbar’s top padding and the Add Note button’s bottom margin to fields. You use those fields to update paddings depending on the insets.
Next, you have to register the OnApplyWindowInsetsListener
that allows you to access the WindowInsets
. Add the following code to the bottom of adaptViewForInsets()
:
// Register OnApplyWindowInsetsListener
root.setOnApplyWindowInsetsListener { _, windowInsets ->
// Update toolbar's top padding to accommodate system window top inset
val newToolbarTopPadding =
windowInsets.systemWindowInsetTop + toolbarOriginalTopPadding
toolbar.updatePadding(top = newToolbarTopPadding)
// Update "Add Note" button's bottom margin to accommodate
// system window bottom inset
addNoteButtonMarginLayoutParam.bottomMargin =
addNoteButtonOriginalBottomMargin +
windowInsets.systemWindowInsetBottom
addNoteButton.layoutParams = addNoteButtonMarginLayoutParam
// Update notes recyclerView's bottom padding to accommodate
// system window bottom inset
notes.updatePadding(bottom = windowInsets.systemWindowInsetBottom)
windowInsets
}
This code updates several things:
- The toolbar’s top padding to accommodate the system window top inset.
- The Add Note button’s bottom margin to accommodate the system window bottom inset.
- The bottom padding of your note list to accommodate the system window bottom inset.
Great! Before testing this, you have to call adaptViewForInsets()
from your onCreate()
. In onCreate()
, below requestToBeLayoutFullscreen()
, add this:
// Adapt view according to insets
adaptViewForInsets()
Build and run your project. You should see something like this:
Notice the position of the Add Note button and how your toolbar handles the status bar.
You should also update the Save Note screen in a similar fashion. First, add these imports to the list of imports in SaveNoteActivity.kt:
import android.view.ViewGroup
import androidx.core.view.updatePadding
Similarly, add the following code below requestToBeLayoutFullscreen()
:
private fun adaptViewForInsets() {
// Prepare original top padding of the toolbar
val toolbarOriginalTopPadding = toolbar.paddingTop
// Prepare original bottom margin of colors recycler view
val colorsLayoutParams = colors.layoutParams as ViewGroup.MarginLayoutParams
val colorsOriginalMarginBottom = colorsLayoutParams.bottomMargin
}
On the Save Note screen, you also have to update your toolbar and the bottom margin of the color list. This code prepares the original margin and padding values that you’ll use later.
Next, add the following code to the bottom of adaptViewForInsets()
:
// Register OnApplyWindowInsetsListener
root.setOnApplyWindowInsetsListener { _, windowInsets ->
// Update toolbar's top padding to accommodate system window top inset
val newToolbarTopPadding = toolbarOriginalTopPadding +
windowInsets.systemWindowInsetTop
toolbar.updatePadding(top = newToolbarTopPadding)
// Update colors recycler view's bottom margin to accommodate
// system window bottom inset
val newColorsMarginBottom = colorsOriginalMarginBottom +
windowInsets.systemWindowInsetBottom
colorsLayoutParams.bottomMargin = newColorsMarginBottom
colors.layoutParams = colorsLayoutParams
windowInsets
}
This code does two things:
- It updates the toolbar’s top padding to accommodate the system window top inset.
- It also updates the color list bottom margin to accommodate the system window bottom inset.
Invoke the method in onCreate()
, below requestToBeLayoutFullscreen()
:
// Adapt view according to insets
adaptViewForInsets()
Build and run your project. You should see something like this:
Notice that the toolbar now handles the system window top inset correctly. However, it’s still difficult to drag the bottom sheet. You’ll improve that next. :]
System Gesture Insets
System gesture insets represent the areas of the window where system gestures take priority. They include the vertical edges for swiping back and the bottom edge for navigating home. You use them to move draggable views away from edges.
To consume system gesture insets, you also have to implement the OnApplyWindowInsetsListener
interface. This time you’ll call getSystemGestureInsets()
instead of getSystemWindowInsets()
.
Leveraging System Gesture Insets
You probably already have a feeling of where you can leverage this. That’s right, it’s your bottom sheet in the Save Note screen.
Add these imports to SaveNoteActivity.kt:
import com.google.android.material.bottomsheet.BottomSheetBehavior
import android.os.Build
import android.view.WindowInsets
import androidx.constraintlayout.widget.ConstraintLayout
Now, add the following code at the beginning of adaptViewForInsets()
in SaveNoteActivity.kt:
// Prepare original peek height of the bottom sheet
val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
val bottomSheetOriginalPeekHeight = bottomSheetBehavior.peekHeight
To update your bottom sheet’s peek height, you have to get a reference to its BottomSheetBehavior
. You also have to store the original peek height.
Add the following method below adaptViewForInsets()
:
private fun adaptBottomSheetPeekHeight(
bottomSheetBehavior: BottomSheetBehavior<ConstraintLayout>,
bottomSheetOriginalPeekHeight: Int,
windowInsets: WindowInsets) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// If Q, update peek height according to gesture inset bottom
val gestureInsets = windowInsets.systemGestureInsets
bottomSheetBehavior.peekHeight = bottomSheetOriginalPeekHeight
+ gestureInsets.bottom
} else {
// If not Q, update peek height according to system window inset bottom
bottomSheetBehavior.peekHeight = bottomSheetOriginalPeekHeight
+ windowInsets.systemWindowInsetBottom
}
}
This code updates your bottom sheet’s peek height for the bottom inset. In Android 10, you use the gesture bottom inset. Below Android 10, you use the window bottom inset to determine how much you have to increase the peek height.
Finally, call the adaptBottomSheetPeekHeight()
you added. Add this code to the bottom of your OnApplyWindowInsetsListener
, right above windowInsets
:
// Update bottom sheet's peek height
adaptBottomSheetPeekHeight(bottomSheetBehavior, bottomSheetOriginalPeekHeight, windowInsets)
Build and run your project. Notice how easy it is to drag out the bottom sheet on the Save Note screen.
Mandatory System Gesture Insets
Mandatory system gesture insets are a subset of system gesture insets. They define areas apps can’t override. In Android 10, only the home gesture zone uses them.
Handling Conflicting App Gestures
This new gesture navigation model may conflict with your app’s current gestures. As a result, you may need to make adjustments to your app’s user interface. So, the last thing you have to do to make your app gesture navigation ready is overriding system gestures.
In NoteMaker, try to select a color for your note. Notice that if you try to scroll through colors by dragging from the right or left edge of the screen, Android triggers the system Back gesture.
You can opt-out of the Back gesture by telling the system which regions need to receive touch input. You can do this by passing a list of Rect
s to the View.setSystemGestureExclusionRects()
API introduced in Android 10.
Add these imports to your list of imports in SaveNoteActivity.kt:
import android.graphics.Rect
import androidx.core.view.doOnLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
Next, add the following code below adaptBottomSheetPeekHeight()
:
private fun excludeGesturesForColors(
bottomSheetBehavior: BottomSheetBehavior<ConstraintLayout>,
windowInsets: WindowInsets) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
bottomSheetBehavior.setBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
// NO OP
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == STATE_EXPANDED) {
// Exclude gestures when bottom sheet is expanded
} else if (newState == STATE_COLLAPSED) {
// Remove exclusion rects when bottom sheet is collapsed
}
}
})
}
}
This code still doesn’t do anything smart. It only lets you execute certain code depending on the bottom sheet’s state.
Now, in excludeGesturesForColors()
, add the following code to the first if-condition below the comment // Exclude gestures...
:
root.doOnLayout {
val gestureInsets = windowInsets.systemGestureInsets
// Common Rect values
val rectHeight = colors.height
val rectTop = root.bottom - rectHeight
val rectBottom = root.bottom
// Left Rect values
val leftExclusionRectLeft = 0
val leftExclusionRectRight = gestureInsets.left
// Right Rect values
val rightExclusionRectLeft = root.right - gestureInsets.right
val rightExclusionRectRight = root.right
// Rect for gestures on the left side of the screen
val leftExclusionRect = Rect(
leftExclusionRectLeft,
rectTop,
leftExclusionRectRight,
rectBottom
)
// Rect for gestures on the right side of the screen
val rightExclusionRect = Rect(
rightExclusionRectLeft,
rectTop,
rightExclusionRectRight,
rectBottom
)
// Add both rects and exclude gestures
root.systemGestureExclusionRects = listOf(leftExclusionRect, rightExclusionRect)
}
This code excludes the Back gesture in the area where the user can scroll through the available colors. In the same method, add the following code to the second if-condition below the comment // Remove exclusion...
:
root.doOnLayout { root.systemGestureExclusionRects = listOf() }
Now when the bottom sheet is collapsed, the system registers the gestures on the whole side of the screen.
Finally, add the following code to the bottom of OnApplyWindowInsetsListener
, right above windowInsets
:
// Exclude gestures on colors recycler view when bottom sheet is expanded
excludeGesturesForColors(bottomSheetBehavior, windowInsets)
Build and run your project. Notice that when the bottom sheet expands, you can’t go back when dragging from the left or right edge of the screen in the area where your color list is. But, when you try to drag from the edge in that same area with the bottom sheet collapsed, you can go back.