Implementing BiDirectional ViewPager by overriding onInterceptTouchEvent & onTouchEvent

Tan Jun Rong avatar

Tan Jun Rong

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. 😃

Wireframe of BiDirectional ViewPager
Wireframe of BiDirectional ViewPager

Demo

BiDirection ViewPager Demo
BiDirection ViewPager 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 VerticalViewPager has a ViewPager.PageTransformer that 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:

VerticalViewPager contains a horizontal ViewPager in each page
VerticalViewPager contains a horizontal ViewPager in each page

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 ViewPagers are 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:

  1. from VerticalViewPager's onInterceptEvent
  2. then to HorizontalViewPager's onTouchEvent
  3. then finally to VerticalViewPager's onTouchEvent

Let's walkthrough the responsibility of each of them.

  1. from VerticalViewPager's onInterceptEvent
    • if we return true here, VerticalViewPager will intercept the event until ACTION_UP is received (lifting of the finger)
    • we should return true if 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
  2. then to HorizontalViewPager's onTouchEvent
    • touch event will reach here, if step 1 return false
    • since ViewPager works horizontally, we do nothing here
  3. 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_DOWN because it is consumed in step 2 in the previous cycle (this can be hard to understand, but you can move on, more details in)
Flow Chart of Touch Event
Flow Chart of Touch Event

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_DOWN is 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 return true or false accordingly
  • Finally, when finger is lifted, ACTION_UP is 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_DOWN in onTouchEvent has to return true in order to listen to the rest of the event. If you return false, your subsequent onTouchEvent won't be fired, until the next ACTION_DOWN is received.
  • Once onInterceptEvent return true, it's children WILL NOT received any onTouchEvent until the next ACTION_DOWN
  • ACTION_DOWN needs to be passed to super.onTouchEvent(), otherwise the ViewPager won't start moving. That is why I need to inject an ACTION_DOWN event to make it work.
    (Reference: BiDirectionViewPager.kt#L72)

Tips

  • ACTION_DOWN --> ACTION_MOVE --> ACTION_UP
    • You must remember, ACTION_DOWN is the first trigger, that is finger's first touch down. Followed by ACTION_MOVE, and it always end with ACTION_UP to complete a whole full cycle. Then things repeat again upon next finger touch down.
  • Turn on Show touches and Pointer location in 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 onInterceptEvent and onTouchEvent, because it is touch event dispatching is complicated 😵. It took me many days, keep going and you will get it working! 👍💪
Developer options: Show touches & Pointer Location
Developer options: Show touches & Pointer Location

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 avatar
Written By

Tan Jun Rong

Android Programmer who likes writing blogs, reading, coffee, snowboarding.
Published in Android
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.