Chris Banes is an Engineer in the Android Developer Relations Team

This is the third installment in our series on gesture navigation. If you want to check out the first two articles, please click the link below:

  • Open full screen experience | Gesture navigation (1)
  • Handling Visual Conflict | Gesture navigation (PART 2)

In the last article, we covered drawing your application from edge to edge. Starting with this article, we’ll show you how to handle conflicts between your application and the new system interaction gestures introduced in Android 10.

First let’s understand what a gesture conflict is. Take a look at an example, such as the following music player app, which allows users to fast-forward or rewind the current song by dragging the SeekBar.

DrawerLayout
ViewPager
SeekBar
Sliding operation

So, how to solve this problem? We’ve prepared a flow chart to help you make quick decisions:

△ Click on the picture to enlarge

Note: Non-sticky Immersion: Users can exit immersion mode by swiping on the system bar. Sticky Immersion mode: Users can temporarily exit immersion mode by swiping on the system bar

Here we further explain the contents of the flowchart.

Question 1: Does the application need to hide the navigation bar or status bar?

The first question in the flowchart asks if the main usage scenario of your application needs to hide the navigation and/or status bar. By “hiding”, I mean making them invisible at all. This does not mean making your application full screen from edge to edge.

Reasons to hide may include:

  • Use the FLAG_FULLSCREEN WindowManager switch. Note that this effect can also be done with the Android :windowFullscreen Theme setting, or by extending a theme.xxx. Fullscreen derivative control.
  • Use the system UI visibility switch SYSTEM_UI_FLAG_FULLSCREEN.
  • Use the system UI visibility switch in Immersion mode: SYSTEM_UI_FLAG_IMMERSIVE or SYSTEM_UI_FLAG_IMMERSIVE_STICKY.

Generally speaking, games, video players, photo apps, drawing apps, etc., will answer “yes” to this question.

Question 2: Do major UIs require sliding operations in/near the interaction area?

This question asks whether the application interface contains any components that require the user to swipe in or near the gestural navigation interaction area. (Includes swiping in the Back and Back home button areas)

Many games usually answer “yes” here because:

  • Controls on a game screen tend to be very close to the left/right edge of the screen, or near the bottom of the screen.
  • Some games require sliding on the screen to manipulate an element that can appear anywhere on the screen, such as platformer action games.

In addition to games, there are some common UIs that might also answer “yes” here:

  • Image crop UI, where control points for crop images may be located near the left/right edge of the screen.
  • A drawing application that allows the user to draw on an on-screen canvas (sliding, of course).

Q3: Are commonly used views/controls located in/near gesture interaction areas?

This should be a simpler question. Note that this problem also includes views/controls that occupy a large area of the screen and include areas for gesture interaction. Such as DrawerLayout or larger ViewPager.

Q4: Does the view/control need to slide and drag interaction?

This follows problem number three. Does the view that answered “yes” in question 3 require the user to slide or drag on it?

There are a number of cases that answer “yes” to this question, including the aforementioned progress bar, the Bottom Sheet, or popupmenus that can be opened by sliding.

Q5: Is the view/control mostly in the gesture interaction area?

Following question 4, further confirm whether the view is completely or mostly within the gesture interaction area.

If your view is placed in a scrollable container (such as RecyclerView), understand this question this way: Is the view entirely or mostly in the gesture interaction area? If the user can scroll the view outside of the gesture interaction area, there should be no interaction conflicts.

You may have noticed that the multi-graph display control (ViewPager) answers “no” here in the flowchart. This is because the width of the gesture interaction area on the left and right sides of the screen is relatively small compared to the width of the overall view (default: 20DP on each side). Generally speaking, the screen width is about 360DP when the phone is held upright, which means that the user’s swiping operation is not affected within the range of 320DP (nearly 90% of the total width). Even with inside and outside margins, users can still swipe through the images.

Q6: Does the view/control overlap with the force system gesture interaction area?

The last question asks whether the control is in the system’s mandatory gesture navigation interaction area. If you’ve read our previous articles, you’ll remember that the “mandatory system gesture interaction area” is the area of the screen where system gestures are always prioritized.

For Android 10, there’s only one mandatory interaction area, and that’s at the bottom of the screen. A swipe in this area takes the user back to the home screen or to other recently used apps. This mandatory interaction area may change in future platform releases, but for now we only need to think about the bottom of the screen.

Common examples of this overlap occur:

  • Modeless bottom popup menus, which often collapse into a smaller view at the bottom of the screen and require swiping.
  • Horizontal page switches at the bottom of the screen, such as the UI for selecting different emojis on the soft keyboard.

OK, now that I have explained the problem in the flowchart, let’s go over the solution presented in the flowchart in detail.

Solution 1: No need to deal with gesture conflicts

The simplest “solution” is just… Do nothing!

Of course, you might be able to tweak it a little (see the next few solutions), but you shouldn’t have a major problem with gesture-enabled apps.

If the flow chart gives you the “do nothing” answer, but you still feel there is a problem with the application, please feel free to let us know.

Solution 2: Move the view/control out of the gesture interaction area

As we mentioned in our last article, we can use the Insets area to tell the application where the gesture area is on the screen. One approach we can use to resolve gesture conflicts is to move the conflicting views out of the gesture navigation interaction area. This is especially important for views near the bottom of the screen, where the system forces gesture interactions and where the application cannot use the heat cut API.

Let’s go back to the music player example mentioned earlier. It includes a progress bar at the bottom of the screen that allows users to fast forward and rewind songs.

△ Sliding from the blue area to the center of the screen is equivalent to the “back” button; Swipe up from the red area to return to the main screen. Note that the red area is the system forced gesture interaction area

Simple solution

The simplest solution to this problem is to add some inner/outer margins and push the progress bar up out of the gesture area. Something like this:

▽ Progress bar no longer conflicts after moving up

To do this, we need to use the new system interaction hotspots API provided in API 29 and the Jetpack Core library V1.2.0 (currently in alpha). In the following code, we increased the bottom margin of the progress bar, which is exactly the height of the system forced interaction area:

ViewCompat.setOnApplyWindowInsetsListener(seekBar) { view, insets ->
    // We'll set the views bottom padding to be the same // value as the system gesture insets bottom value view.updatePadding( bottom = insets.systemGestureInsets.bottom ) insets }Copy the code

You can also check out another of our blog posts where we explore ways to make WindowInsets easier to use.

  • How to Make WindowInsets Easier to use: medium.com/androiddeve…

A better solution

After the last step, you might feel like you’ve solved the problem. For some layouts, this is likely to be the ultimate solution. However, after the above changes, a lot of space was wasted under the progress bar, making the UI look less complete. Therefore, in addition to directly modifying the margins of the view, we can also modify the layout to avoid wasted space:

Move the progress bar to the top of the view

Note, however, that we still need to insert an inner margin at the bottom of the play control equal to the height of the system bar so that text such as song titles won’t be obscured by the system navigation bar (the “line” at the bottom of the screen).

Solution 3: Use the gesture area exclusion API

As we mentioned in our last article, “Apps can cut out parts of the system’s gesture area to respond to their own gesture interactions.” This is the new gesture area exclusion API introduced in Android 10.

Apps can use the new System gesture exclusion API in Android 10 to make parts of the edge of the system unresponsive to system gestures. System provides two different functions to “cut out” interaction regions: the setSystemGestureExclusionRects () and the Window. The setSystemGestureExclusionRects (). Which one to use depends on your application: if you are using an Android View, the View API is recommended, otherwise use the Window API.

The main difference between the two apis is that the Window API evaluates the rectangle in Window coordinates. If you are using the View API, you will operate in the View coordinate system. The View API will help you with conversion between coordinate Spaces.

Going back to the music player example mentioned earlier, we now move the progress bar above the control and cover the entire screen width. At this point, the system gesture interaction conflict at the bottom of the screen has been resolved, but the “back” action on the left and right sides of the screen still conflicts with the progress bar:

The gesture area exclusion API is usually called in two places: when the view is laid out (onLayout) or when the view is drawn (onDraw). Your view passes in a List of rectangular areas that should be cut out (that is, not responsive to system gestures). As mentioned earlier, these rectangles must be in the view’s own coordinate system.

Typically, you create a method similar to the following, which is called on onLayout() and/or onDraw() :

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+ if (Build.VERSION.SDK_INT < 29) return // First, lets clear out any existing rectangles gestureExclusionRects.clear() // Now lets work out which areas should be excluded. For a SeekBar this will // be the bounds of the thumb drawable. thumb? .also { t -> gestureExclusionRects += t.copyBounds() } // If we had other elements in this view near the edges, we could exclude them // here too, by adding their bounds to the list // Finally pass our updated list of rectangles to the system systemGestureExclusionRects = gestureExclusionRects }Copy the code
  • The complete code for the above example:

Gist.github.com/chrisbanes/…

Once you’ve done this “cut out” operation, it’s no problem to fast-forward/rewind near the edge of the screen:

Note: SeekBar will actually perform the above cutout for you automatically in Android 10, so you don’t need to do it in SeekBar. This is just an example to show you how to handle conflicts.

Limiting conditions

While the gesture area exclusion API might seem like the perfect solution to all gesture conflicts, it’s not. By using this API, you are essentially declaring that the applied gesture is more important than system actions such as “return”. This is only recommended when no other solution is available.

Because this API somewhat breaks the user’s habit, the system has a limit: each edge of the screen can only be cut by 200dp.

When developers hear about this limitation, they often ask the following questions:

Why should there be limits?

We believe that developers need to make sure that users interact with the system using consistent actions, such as swiping inward from the edge to return. Be careful to maintain consistency across the device, not just within an application. This may seem draconian, but if an app can make the entire edge of the screen unresponsive to system gestures, it will confuse users and the app will most likely be uninstalled.

Again, system navigation must always be consistent and available.

Why 200dp?

The decision logic behind 200DP is very simple. As we mentioned earlier, the gesture area exclusion API should only be used as a last resort, so we calculated the area of touch objects that might need to apply this mechanism. The minimum recommended size for touch objects is 48dp. We take four touch objects, so 4 × 48dp = 192dp. And then we add a little bit of surplus, which is 200dp.

What if the developer asks to cut more than 200dp on the edge?

The answer is that the system will only honor the 200DP at the bottom of your request, as shown below:

The developer requested a cut of 50 + 50 + 125 + 50 dp, but the system only paid the bottom 200dp

Does this limit apply to my view that is not on screen?

No, the system only computes the screen – wide cut rectangles. Similarly, if only part of the view is displayed on-screen, only the on-screen visible portion of the requested rectangle is counted.

Stay tuned for the next installment

After reading this article, you may be asking: Why haven’t we covered the right half of the flowchart yet? This is because the right half is suitable for applications that require full screen drawing, which we’ll cover in our next series on gesture navigation, so stay tuned.

Click here toLearn more about Android gesture navigation