Gesture management: handling gesture conflicts. Part 3

A translation of the article was prepared in advance of the launch of an advanced and basic Android development course.



This is the third article in a series about gesture control. You can familiarize yourself with the translations of the first and second parts if you missed them.

In previous articles, we covered the topic of filling the screen space from edge to edge. In this article, we will look at how to handle any conflicts that arise between the gestures of your application and the new system gestures in Android 10 .

What do we mean by gesture conflicts? Let's look at an example. Let's say we have a music player that allows the user to scroll through the current song by dragging and dropping SeekBar.



Unfortunately, it SeekBaris too close to the area of ​​the gesture of returning to the home screen, because of which the gesture of quickly switching to the previous application (QuickSwitch) begins, which gives the user inconvenience.

The same thing can happen on any edge of the screen where gesture areas are located. There are many common examples that can cause conflicts, such as: Navigation drawers ( DrawerLayout), carousels ( ViewPager ), sliders ( SeekBar), swipe in lists.

Which brings us to the question "how can we fix this?" To facilitate this question, we have created a flowchart that offers you an answer based on the situation.


You can find a PDF version of the flowchart here .

I hope that the questions do not require explanation, but just in case, I will go through each of them:

1. Does the application need to hide the navigation and status bars?


The first question is whether the main use case for your application requires hiding the navigation and / or status bars. By hiding, we mean that these system panels are not visible at all. This does not mean that you have implemented the concept of “edge to edge” or something similar in your application.

Possible reasons to answer “yes” to this question:


Common examples of applications that should answer “yes” to this question are games, video players, photo viewers, drawing applications.

2. The main scenario for using the UI involves swipes in / around the gesture area?


This question finds out if your UI contains any elements in / next to gesture areas (both “back” and “home”) that the user must swipe.

Games usually say yes due to the fact that

  • The controls on the screen are usually located near the left / right edge and the bottom of the screen.
  • In some games, you need to swipe over elements that may be in any place on the screen.

In addition to games, common examples of UIs that should say yes are:

  • UI crop photos where draggable frames are also near the left and right edges of the screen.
  • A drawing application where the user can draw on a canvas that covers the entire screen.

3. Frequently used view in / around gesture area?


I hope this is a fairly simple question. This also includes views, which cover the area of ​​gestures, and then extend to a large part of the screen, for example, DrawerLayoutor a large one ViewPager.

4. View involves swipe / drag?


We change tactics a bit and start looking at individual views. For any of the views for which you answered in the affirmative to the third question, we make a small clarification: should the user swipe / drag it?

There are many examples where you have to answer "yes»: SeekBars, BottomSheet or even PopupMenu(you have to drag to open).

5. Is the view fully / generally located under the gesture areas?


Based on the fourth question, we now clarify whether the view is fully or mainly located in the gesture area.

If your view is in a scrollable container like this RecyclerView, think about this question a little differently: does the fully / basically expanded view fall under the gesture area in all scroll positions ? If the user can scroll the view from under the gesture area, then you do not need to do anything.

In the diagram above, you might notice a carousel full screen ( ViewPager) as an example of a negative answer and wonder why this case does not need to be handled. This is due to the fact that the areas of gestures left / right are relatively small in width (default: 20dpeach) compared to the width of the view. Typicalthe width of the screen of your phone in portrait orientation is ~ 360dp, leaving ~ 320dp of free space in which the user does not have difficulty (this is almost 90% of the screen). Even with internal margins / indents, the user can still comfortably scroll through the carousel.

6. Does the view border overlap any required gesture areas?


The last question clarifies whether the view is under any of the required gesture areas. If you recall our previous article , you will recall that the mandatory areas of system gestures are areas of the screen where system gestures always take precedence.

In Android 10, there is only one mandatory gesture area, which is located at the bottom of the screen, which allows the user to either return home or open their latest applications. This may change in future releases of the platform, but now we only need to work with view at the bottom of the screen.

Typical examples are:

  • Modeless BottomSheet , as they tend to collapse into a small drag-and-drop view at the bottom of the screen.
  • A horizontally scrolling carousel at the bottom of the screen, for example, an interface with stickers.

Now that we’ve sorted out the questions, we hope that you can come to one of the solutions, so let's look at each of them in more detail.

No conflicts to be handled


Let's start with the simplest “solution,” just don't do anything !

Of course, maybe there is still room for optimizations that you can do (see the section below), but fortunately there are no serious problems using the application with the gesture navigation mode turned on.

If the schedule has brought you here, but you still feel that there is a problem, please let us know . Maybe we missed something.

Moving view from gesture areas


As we learned from our previous article , insets exist to tell your application where the system gesture zones are on the screen. One of the methods that we can use to resolve gesture conflicts is to move any conflicting views from gesture areas. This is especially important for the view at the bottom of the screen, since this area is the zone of mandatory gestures, and applications cannot use the gesture exclusion APIs there.

Let's take another look at an example. We have a music player interface that we showed above. It contains SeekBar, located at the bottom of the screen, allowing the user to scroll through the song.


UI music player with a SeekBar at the bottom of the screen

But when a user tries to skip a song, this happens:


Recording a system gesture that conflicts with SeekBar

This is because the lower area of ​​the gesture overlaps the SeekBar, so the home-back gesture takes priority. Here is the visualization of the gesture zones:



A simple solution


The simplest solution here is to add an extra indent / margin so that the SeekBar moves up from the gesture area. Something like this:



If we drag the SeekBar in this example, you will see that we no longer activate the homecoming gesture:


SeekBar no longer conflicts with the lower system gesture.

To implement this, we need to use the new system gesture insets available in API 29 and Jetpack Core library v1.2.0 (currently in alpha ). In the example, we increase the bottom indent SeekBarto match the value of the lower gesture inset :

ViewCompat.setOnApplyWindowInsetsListener(seekBar) { view, insets ->
     //      view ,    system gesture insets
    view.updatePadding(
        bottom = insets.systemGestureInsets.bottom
    )
    insets
}

If you are interested in learning how to make it easier to work with WindowInsets, you can read our other article on this topic:

WindowInsets - Listeners layout

Further actions


At this point, you can decide that the job is already done, and for some layout this may very well be the final solution. But in our example, the user interface visually regressed with a lot of lost space under SeekBar. Thus, instead of simply pushing the view up, we can instead redesign the layout to avoid losing space:


SeekBarmoved to the top of the playback panel.

Here we have moved SeekBarto the top of the playback panel, completely outside the gesture area. This means that we no longer need to artificially increase the height of the panel to accommodate SeekBar.

, . « ».

API


In our previous article, we mentioned that "applications have the ability to exclude system gestures for certain parts of the screen . " Applications do this using the gesture exclusion APIs, which first appeared in Android 10.

There are two different functions that the system provides to exclude gesture areas: View.setSystemGestureExclusionRects()and Window.setSystemGestureExclusionRects(). What you should use depends on the application: if you use Android View, the system prefers the view API, otherwise use the WindowAPI.

The main difference between the two APIs is that the Window APIexpects any rectangles to be in the coordinate space of the window. If you use view, you will usually work in the view coordinate space instead. The View API takes care of the transformation between coordinate spaces, that is, you need to reason only in terms of the content of the view.

Let's look at an example. We are going to use our example of a music player again, which SeekBaris located across the entire width of the screen. We fixed the conflict SeekBarwith the gesture of returning to the home screen in the previous section, but we still have left and right areas of gestures that we need to take care of.

Let's see what happens when a user tries to skip a song when the “slider”SeekBar(circular drag) is located near one of the edges:


SeekBar has a conflict with the back gesture area.

Since the slider is under the right gesture area, the system believes that the user is trying to go back using the gesture, therefore, shows the back arrow. This is inconvenient for users, as they probably did not want to go back at the moment. We can fix this using the gesture exclusion APIs mentioned above to exclude the borders of the slider.

Gesture exception APIs are usually called from two places: onDraw()when your view is rendered, and onLayout()otherwise. Your view passesList<Rect>containing all the rectangles to be excluded. As mentioned earlier, these rectangles must be in their own view coordinate system.

Usually you create a function like this that will be called from onLayout()and / or onDraw():

private val gestureExclusionRects = mutableListOf<Rect>()
private fun updateGestureExclusion() {
   //   ,      Android 10+
   if (Build.VERSION.SDK_INT < 29) return
  // -,     
   gestureExclusionRects.clear()
      //   ,    .  SeekBar    .
   thumb?.also { t ->
       gestureExclusionRects += t.copyBounds()
   }
   //            view,       ,     
   // ,       
   systemGestureExclusionRects = gestureExclusionRects
}

A complete example can be found here .

After we added this, rewinding near the edges works as expected:


SeekBar working in the back gesture area

Note on the example above. SeekBaralready does this automatically for you in Android 10, so there is no need to do it yourself. Here we do it just as an example to show you the general outline.

Limitations


Although the gesture exclusion APIs may seem like the perfect solution for resolving all gesture conflicts, this is actually not the case. Using the gesture exclusion APIs, you declare that your application’s gesture is more important than system return actions. This is a strong statement, so this API is designed to be an emergency exit when you can do nothing else.

Using the gesture exclusion APIs, you declare that the gesture of your application is more important than the system action of backtracking.

Since the behavior that the API provides can violate a comfortable user experience, the system limits its use: applications can exclude only up to 200dp per edge.

Here are some common questions that developers have when they hear this:

Why is the restriction necessary? I hope the explanation above has already led you to a reason. We believe that it is very important for the user to be able to consistently go back from the side swipe. Consistently throughout the device, not one application. This restriction may seem too restrictive, but just one application, excluding the entire edge of the screen, is enough to cause inconvenience to the user, which leads either to the removal of the application, or to something more radical.

In other words, the navigation system should always be consistent and easy to use.

Why 200dp?The argument for 200dp is pretty straight forward. As we mentioned earlier, the gesture exclusion APIs are intended to be used as a last resort, so this limit was calculated as a multiple of several important touch targets. The minimum recommended size for a sensory target is 48dp.4 sensory targets × 48dp = 192dp. Add some more indentation and we get the value 200dp.

What if I need to exclude more than 200dp per edge? The system will exclude only the lowest 200dp that you requested.


The system allows a request for a total height of 200 dp, counting from the bottom edge of

My view outside the screen, is this considered a limit?No, the system only considers excluded rectangles that are within the screen. Similarly, if the view is partially on the screen, only the visible part of the requested rectangle is taken into account.

Dive into the next post


Perhaps after reading this, you are wondering why we did not consider the right side of the flowchart. The solutions below are designed specifically for applications that use the entire screen in rendering. We will talk about them in the next part.






All Articles