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 ViewPager
s 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 aViewPager.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:
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 ViewPager
s 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:
- 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
true
here, VerticalViewPager will intercept the event untilACTION_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
- if we return
- then to HorizontalViewPager's
onTouchEvent
- touch event will reach here, if step 1 return
false
- since
ViewPager
works 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_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)
- 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_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 returntrue
orfalse
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
inonTouchEvent
has to returntrue
in order to listen to the rest of the event. If you returnfalse
, your subsequentonTouchEvent
won't be fired, until the nextACTION_DOWN
is received. - Once
onInterceptEvent
returntrue
, it's children WILL NOT received anyonTouchEvent
until the nextACTION_DOWN
-
ACTION_DOWN
needs to be passed tosuper.onTouchEvent()
, otherwise theViewPager
won't start moving. That is why I need to inject anACTION_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 byACTION_MOVE
, and it always end withACTION_UP
to complete a whole full cycle. Then things repeat again upon next finger touch down.
- You must remember,
- Turn on
Show touches
andPointer 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
andonTouchEvent
, 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.