Controlling swipe direction with ViewPager2
The new ViewPager2 API is a replacement for the good old ViewPager API that was used to create swipeable views in Android. ViewPager2
has a lot of benefits over its predecessor like support for vertical swiping, the possibility of using the diff-util and the dynamic number of fragments, etc. It is also the recommended way of creating swipeable views on Android moving forward.
In one of the places where we are using ViewPager
in our app, we enable/disable user swipe at runtime in either(left/right) or both directions. This is done on basis of different user input. In ViewPager
we did this by sub-classing ViewPager
class and then overriding onTouchEvent
and onInterceptTouchEvent
. As many of the stackoverflow answers suggest it is possible to determine the direction of swipe that user is attempting and then simply discard those touch events.
We wanted to migrate this screen to ViewPager2
. However, it is a final
class that cannot be subclassed. This was a blocker for our migration till the time we figured a solution.
Before jumping on to the solution it is important to understand that unlike ViewPager
, ViewPager2
is essentially a heavily customized RecyclerView
. It uses RecyclerView
’s superpowers to render and reuse views. In fact, a lot of the code was removed from ViewPager
and all that responsibility was given to RecyclerView
instead.
Now that we know that RecyclerView
handles view rendering, we can use RecyclerView
’s APIs to intercept swiping as well. RecyclerView has a useful method addOnItemTouchListener
which takes an object of RecyclerView.OnItemTouchListener
. This allows us to intercept and process touch events on RecyclerView.
We will create a subclass of RecyclerView.OnItemTouchListener
and call that SwipeControlTouchListener
. To control swipe direction at runtime let’s first create an enum call to specify all possible swipe direction combinations.
enum class SwipeDirection {
ALL, // swipe allowed in left and right both directions
LEFT, // swipe allowed in only Left direction
RIGHT, // only right
NONE // swipe is disabled completely
}
This enum enables us to specify directions in which a user is allowed to swipe at any given point in time. Now we need to create a function that takes MotionEvent
object and attempts to detect the direction user is trying to swipe. It should then compare it with the currently set SwipeDirection
and return true
if the user’s intention and current setting match or false
otherwise. Here is that function
private fun isSwipeAllowed(event: MotionEvent): Boolean {
if (direction === SwipeDirection.ALL) return true
if (direction == SwipeDirection.NONE) //disable any swipe
return false
if (event.action == MotionEvent.ACTION_DOWN) {
initialXValue = event.x
return true
}
if (event.action == MotionEvent.ACTION_MOVE) {
try {
val diffX: Float = event.x - initialXValue
if (diffX > 0 && direction == SwipeDirection.RIGHT) {
// swipe from left to right detected
return false
} else if (diffX < 0 && direction == SwipeDirection.LEFT) {
// swipe from right to left detected
return false
}
} catch (exception: Exception) {
exception.printStackTrace()
}
}
return true
}
For the most part, this is self-explanatory. Let us look at the most interesting bit. We are saving the current x
coordinate when an ACTION_DOWN
event is received. That is, the user has touched the screen to begin swipe. After that, when the user begins the swipe, an ACTION_MOVE
event is received. Here we can take the diff between the initial x position and current x position to figure out what direction the user is trying to swipe to, then compare it with the current setting and take a decision accordingly. I have left out other details of SwipeControlTouchListener
for brevity. You can find full implementation here.
Once this touch listener is set on VP2’s RecyclerView, changing swipe control is a matter of just setting the swipe direction on this touch listener as follows.
swipeControlTouchListener.setSwipeDirection(SwipeDirection.LEFT)
Now here is a little hacky part where I had to get access to ViewPager2
to RecyclerView in a questionable way.
// apply touch listener on ViewPager RecyclerView
val recyclerView = binding.viewPager[0] as? RecyclerView
if (recyclerView != null) {
recyclerView.addOnItemTouchListener(swipeControlTouchListener)
} else {
Log.w(localClassName, "RecyclerView is null, API/Version changed ?!")
}
Here we are relying on RecyclerView
being the first child of VP2 which is an implementation detail and can change with library upgrades. One solution to this problem could be to use findViewById
but, that’s not possible because the id of this RecyclerView is generated at runtime using ViewCompat.generateViewId()
. If you take a look at VP2’s source code you will know what I mean.
This is not a 100% perfect solution but a pretty good workaround for us for time being. This is also because I don’t expect too many changes in ViewPager2’s layout anytime soon, also, it will be using RecyclerView
for the forseeable future. There is one other solution discussed in this stackoverflow thread and here is a feature request that I filed on Google bug tracker for this.
If you think this can be improved let me know in the comments.
Sample App with source: SwipeControlViewPager2
Same result with Gesture Overlay:
We also tried a GestureOverlay
based approach which works pretty well in most of the cases. We ran into some minor issues related to child view scrolling and fast swiping. In interest of time we finalised on approach discussed above. You can deep dive on GestureOverlay based approach here.
Credit:
The SwipeControlTouchListener
is slightly modified version of this stackoverflow answer.
Happy Coding!