Black Lives Matter. Read our statement here.
Home · Android & Kotlin Tutorials

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.

5/5 4 Ratings

Version

  • Kotlin 1.3, Android 5.0, Android Studio 3.4

In a world of fragmented devices, manufacturers want modern UIs. Often each manufacturer implements a different solution, making app design and development tough. Developers needed a way to unify the ecosystem. In Android 10, Google added Gesture Navigation, a new system navigation mechanism that replaces the button navigation alternatives with three, unique gestures.

In this tutorial, you’ll learn how to add support for Gesture Navigation to your app by working with a simple app for creating and managing notes: NoteMaker. You’ll learn how to:

  • Make your UI edge-to-edge, allowing your app to display its content behind the status and navigation bars.
  • Leverage insets, determining how much to inset your content to accommodate gesture areas.
  • Override gestures in special cases.

For this tutorial, you need Android Studio and an Android device or emulator with Android 10.

Note: This tutorial assumes you have some previous knowledge of Kotlin and Android programming. If you’re starting with them, check out Kotlin for Android or Your First Kotlin Android App to get familiarized with the language and platform.

Learning the Gestures

Before you start writing code to implement the gestures, it is helpful to learn what the gestures are and how they work. Check them out Below!

The navigating back gesture:

View of map changes to home screen with a swipe from the left.

Swiping in from an edge takes you back. Watch the left edge of the screen. You can swipe in from the left or right edge to go back.

The navigating home gesture:

View of map is swiped up from the bottom and away, leaving the home screen.

Swiping up from the bottom is the gesture for home. Watch the bottom edge of the screen.

The recent apps or overview gesture:

A swipe from the middle shrinks the map view and displays a small Google Maps icon on the smaller map.

Finally, swiping up and holding in the middle takes you to your recent apps or overview. Watch the bottom edge of the screen. Now that you’ve seen the gestures, it’s time to see how they are implemented in an app.

Getting Started

Start by downloading the materials by clicking the Download Materials button at the top or bottom of the tutorial. Open the begin project in Android Studio. You’re ready to go!

To get yourself familiar with the app, enable 3-button navigation in your phone settings. You can find this option in System ‣ Gestures ‣ System navigation. Don’t worry, you’ll switch to gesture navigation in a few moments.

Build and run your project in Android Studio. You’ll see the Note Overview screen. Take your time and explore the app before you continue.

A blank screen with notes in a header at the top and a round plus button on the bottom right.

Click the button on the bottom right corner and you’ll see the screen where you can edit your note. You can pick a color for your note or delete the note by using the controls in the bottom sheet. Edit the note’s title and content according to the image below. Select the color for the note.

A note titled My First Note and with the text Let's make this note yellow. A selection of small colored squares at the bottom of the screen.

Finally, click the check-mark button on the top right corner to save the note. On the Note Overview screen, you can see the note you created.

The previously blank note screen with the My First Note information in a yellow box.

Seeing the Problem at Hand

Now, you’ll see why you need to check your app’s behavior with gesture navigation enabled. Go to your device settings and search for System navigation. You’ll find a screen like this:

System navigation selection page from settings offering the choice of fully gestural, 2-button or 3-button navigation

Switch your system navigation to Fully gestural navigation and go back to the app. Click the note you created and try to change its color. Notice how you have to struggle to drag the bottom sheet bar if you even can drag it at all.

Yellow My First Note screen shrinks and floats above home screen.

Go back to the Note Overview screen by clicking the check-mark button on the top right corner or by swiping in from the left or right edge of the screen.

Great! Now that you are familiar with the app, and you saw what problems this new system navigation could cause, you’re ready to add support for gesture navigation to NoteMaker!

Making Your App Edge-to-Edge

In a typical Android app, the bounds of the app are below the status bar and above the navigation bar.

White screen black navigation and status bars. A gold, horizontal line crossing the bidding of the screen and another vertical line across the left fifth of the page.

When making your app edge-to-edge, you want the app to display its content as if the navigation and status bars weren’t there. That creates a more immersive experience.

In Android 10, Google recommends drawing behind the navigation bar and the status bar. So, in edge-to-edge your app’s bounds look like this:

White screen with no navigation or status bars. A gold, horizontal line crossing the bidding of the screen and another vertical line across the left fifth of the page.

Changing System Bar Colors

The first thing you need to do when making your app UI edge-to-edge is to change the system bar colors. Open styles.xml and look inside. You’ll see your base app theme AppTheme.

Add the following items at the bottom of the AppTheme:

<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>

Alternatively, you can do this dynamically by using Window.setNavigationBarColor() and Window.setStatusBarColor() in the onCreate().

Build and run your project. See that your system bar colors are now transparent.

White screen with white, blank bars at top and bottom, a green header saying Notes and a yellow rectangle with the My First Note information.

Requesting Fullscreen Layout

Before you can lay out the view edge-to-edge, your app must tell the system that it can handle this type of view. You can do this by using View.setSystemUiVisibility() to set the following flags:

  • SYSTEM_UI_FLAG_LAYOUT_STABLE
  • SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.

At the top of NotesOverviewActivity.kt, add the following import to the list of imports:

import android.view.View

Now, add the following code below onCreate():

private fun requestToBeLayoutFullscreen() {
  root.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or 
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
}

These flags tell the system to display your app fullscreen as if the navigation and status bars weren’t there. You can also set SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN. That allows you to draw behind the status bar.

Finally, call the code you’ve added from onCreate(). Add the following code to onCreate(), right below setContentView():

// Request to be layout full screen
requestToBeLayoutFullscreen()

Nice! Build and run your project. See that your content is now edge-to-edge.

White Notes screen with Notes in a green header and no navigation or status bars.

Repeat the same three steps in SaveNoteActivity.kt. After that, build and run your project. Open the note you created at the beginning of the tutorial and you should see something like this:

Yellow My First Note screen with green header and no navigation or status bars.

Leveraging Insets

After running your app and looking at the modified activities, you can see that some of your views are behind the system bars. This is where insets come into play.

Insets are a collection of values that tell you how much to inset, or move, content. There are two types of insets: System window insets and system gesture insets.

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.

Handling Landscape Orientation

When making your app edge-to-edge, remember to check how your app behaves in landscape orientation.

Enable 3-button navigation and remove android:screenOrientation="portrait" for your activities in AndroidManifest.xml. Run the app in the landscape orientation. You’ll get something like this, which obviously isn’t great:

Landscape view of Notes screen with yellow My First Note box and 3 button navigation incorrectly displayed on left edge.

This tutorial doesn’t cover how to handle landscape orientation. You can experiment with landscape yourself by applying the techniques we used for portrait. You’ll have to play some more with the padding in the landscape orientation, but it’s an excellent challenge to practice what you’ve learned. :]

Testing 3-Button Navigation and Gestural Navigation

Well done! You’ve adapted your app to be nav-ready. Finally, test your app in both navigation modes.

Use the device settings like you did in the previous steps to try each navigation mode and observe how the app behaves in each. When finished, your app will behave like this:

Gif of user using gestural navigation to choose a note color, navigate to note screen and back to settings.

Where to Go From Here?

Wow, that was a lot of work!

You can download the completed project files by clicking on the Download Materials button at the top or bottom of the tutorial.

If you want to check out some more materials on this topic, please check this talk from the Google I/O’19 and Official Android documentation.

I hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments