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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.

Blank screen it blank, peach status and navigation 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:

White screen with green Notes header all the way at the top, the M First Note information in a yellow box and a round plus button at the bottom right of the screen.

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:

Yellow screen with My First Note info and green Save Note bar at the top.

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.

White screen with peach insets on left, right and bottom edges of screen.

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.

Yellow My First Note screen with green Save Note bar at top. Colored squares swipe up from bottom of the 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.

Blank white screen with peach inset on the bottom edge.

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.

Gif showing that a swipe from the right to select a color rectangle accidentally brings you back to the main notes screen.

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 Rects 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.

Gif showing a swipe from the right to select a color not leading to a back gesture.