Implementing BiDirectional ViewPager by overriding onInterceptTouchEvent & onTouchEvent
BiDirectional ViewPager
Android provides us with many widgets for the UI, but sometimes we need a particular behaviour that cannot be achieved by the provided ones. That is when we need to extend the existing one and make a custom view.
Today I need to code a ViewPager which can be swiped in both horizontal and vertical direction. So I need to make a custom view for it. In this post, I will share how it is made. 

Demo

Vertical ViewPager
To achieve this, we need to nest 2 ViewPagers together. The first ViewPager is for the vertical scrolling, let's call it VerticalViewPager. I found an answer in Stackoverflow for achieving this.
In each page of the VerticalViewPager, I place a horizontal ViewPager in it.
Here's a quick explanation of how it works:
- The
VerticalViewPagerhas aViewPager.PageTransformerthat swaps the X-translation into Y-translation - Take a look at
transformPage()in the SO link and the code is pretty straight forward
Horizontal ViewPager
Now we have a vertical ViewPager. Next, to achieve the bi directional scrolling, we need to nest horizontal ViewPager inside the vertical one.
The diagram shows how it works:

Since ViewPager works horizontally out of the box, so we don't need any special tricks to make it move horizontally. However, when we nest ViewPagers like this, there is a problem:
PROBLEM: both of the
ViewPagersare trying to listen to the event!
Therefore, one of the ViewPager won't be working, because the touch event is stolen by the other ViewPager.
overriding onTouchEvent & onInterceptEvent
To solve the problem of both VerticalViewPager and horizontal ViewPager trying to listen to the swipe event, we need to properly distribute the touch event. The 2 important methods for achieving this is the onTouchEvent and onInterceptEvent event. I learned about how these 2 methods work from this tutorial: http://balpha.de/2013/07/android-development-what-i-wish-i-had-known-earlier/
To understand the rest of following post, you can head over to this link above.
️
Distributing the touch event from Vertical ViewPager to Horizontal ViewPager
After reading the link, you should understand that parent view's onInterceptEvent will be run first, followed by the children's onTouchEvent, and followed back to the parent view's onTouchEvent.
In our case, the touch event travels like this:
- from VerticalViewPager's
onInterceptEvent - then to HorizontalViewPager's
onTouchEvent - then finally to VerticalViewPager's
onTouchEvent
Let's walkthrough the responsibility of each of them.
- from VerticalViewPager's
onInterceptEvent- if we return
truehere, VerticalViewPager will intercept the event untilACTION_UPis received (lifting of the finger) - we should return
trueif it is a vertical swipe - we should return
false, if it is a horizontal swipe - so in this method, we need to write a simple gesture detection to determine if the swipe is Vertical or Horizontal
- if we return
- then to HorizontalViewPager's
onTouchEvent- touch event will reach here, if step 1 return
false - since
ViewPagerworks horizontally, we do nothing here
- touch event will reach here, if step 1 return
- then finally to VerticalViewPager's
onTouchEvent- touch event will reach here, if step 1 return
true - we need to swap the X and Y input of the touch event to make it scroll vertically
- we need to inject
ACTION_DOWNbecause it is consumed in step 2 in the previous cycle (this can be hard to understand, but you can move on, more details in)
- touch event will reach here, if step 1 return

Show Me The Code!
Code for Step 1
So finally, the code for step 1 looks like this:
(Reference: BiDirectionViewPager#onInterceptTouchEvent()#L41)
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
val action = event.actionMasked
val currentPoint = Point(event.x.toInt(), event.y.toInt())
if (action == MotionEvent.ACTION_DOWN) {
// mark the beginning, when finger touched down
initialTouchPoint = Point(currentPoint)
} else if (action == MotionEvent.ACTION_UP) {
// reset the marking, when finger is lifted up
initialTouchPoint = Point(0, 0)
} else {
val moveDistance = currentPoint.distanceFrom(initialTouchPoint)
if (moveDistance > FINGER_MOVE_THRESHOLD) {
val direction = MotionUtil.getDirection(initialTouchPoint, currentPoint)
// check if the scrolling is vertical
if (direction == MotionUtil.Direction.up || direction == MotionUtil.Direction.down) {
return true
}
}
}
return false
}
Explanation
The chunk of code can be divided into 3 if...else... block:
- When the finger is touched down,
ACTION_DOWNis received, I save the coordinates - After that, I calculate the angle in the
else block, to determine whether it is a vertical swipe or horizontal swipe, and returntrueorfalseaccordingly - Finally, when finger is lifted,
ACTION_UPis received, I clear the saved coordinate, so that the cycle can repeat again upon the next finger touch down.
Code for Step 2
We don't need any code here, just use the normal ViewPager!
Code for Step 3
The code for step 3 can be found here:
(Reference: BiDirectionViewPager#onTouchEvent#L65)
override fun onTouchEvent(event: MotionEvent): Boolean {
// swapping the motionEvent's x and y, so that when finger moves right, it becomes moving down
// for VerticalViewPager effect
event.swapXY()
// this portion is used for injection ACTION_DOWN
if (firstTime && event.actionMasked == MotionEvent.ACTION_MOVE) {
injectActionDown(event)
firstTime = false
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
firstTime = true
}
super.onTouchEvent(event)
return true
}
Explanation
We only need to do 2 things here. Firstly, we need to swap the X and Y input of the touch event to make Vertical ViewPager. Secondly, we need to inject ACTION_DOWN event because it is already consumed by the horizontal ViewPager. After that, just delegate the event to super.touchEvent() to do it's job, and return true saying that we've consumed the event.
Caveat (optional read)
The following few points took me days to figure out. Perhaps you won't understand it on the first read, but if you are stuck, come back and read the following few points, it might help!
-
ACTION_DOWNinonTouchEventhas to returntruein order to listen to the rest of the event. If you returnfalse, your subsequentonTouchEventwon't be fired, until the nextACTION_DOWNis received. - Once
onInterceptEventreturntrue, it's children WILL NOT received anyonTouchEventuntil the nextACTION_DOWN -
ACTION_DOWNneeds to be passed tosuper.onTouchEvent(), otherwise theViewPagerwon't start moving. That is why I need to inject anACTION_DOWNevent to make it work.
(Reference: BiDirectionViewPager.kt#L72)
Tips
-
ACTION_DOWN-->ACTION_MOVE-->ACTION_UP- You must remember,
ACTION_DOWNis the first trigger, that is finger's first touch down. Followed byACTION_MOVE, and it always end withACTION_UPto complete a whole full cycle. Then things repeat again upon next finger touch down.
- You must remember,
- Turn on
Show touchesandPointer locationin Developer options, it helps in the angle and threshold calculation. There will be indications when you touch or move your finger on the screen once you have them turned on. - Lastly, don't give up!
- It can be frustrating dealing with
onInterceptEventandonTouchEvent, because it is touch event dispatching is complicated
. It took me many days, keep going and you will get it working! 
- It can be frustrating dealing with

Code Sample is Available at Github!
The link is here: BiDirectionViewPager.kt. Once you clone the project and run it, there will be an example that looks like the demo at the top of this post.
Hope you enjoy the post,
See you in the next post! 
Tan Jun Rong
Clap to support the author, help others find it, and make your opinion count.