Dynamically preventing scrolling on selected ViewPager pages

ViewPagers are an extremely powerful UI tool that by default can be swiped left and right freely. In some cases however, it can be useful to prevent the user swiping in certain directions on certain pages, i.e. a “LockableViewPager”. For example, the first 2 pages might have to be passed programmatically, and then all other pages can be navigated between freely.

This article will implement determining and changing at any time the current permitted swipe direction(s) (left, right, both, neither) using a custom ViewPager, concluding with a full use case. The end result of this article is also available as a Gist.

Including custom element

Just replacing ViewPager with the full path of your LockableViewPager is the only change needed in your layout XML.

<com.example.LockableViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

LockableViewPager

First, a new class extending ViewPager has to be created, as well as values for the initialXValue (used to determine swipe direction) and direction (used to store permitted swipe direction):

class LockableViewPager(context: Context, attrs: AttributeSet) : ViewPager(context, attrs) {

    private var initialXValue: Float = 0f
    private var direction: SwipeDirection? = null

Additionally, an enum of the possible scroll directions needs to be defined outside the class:

enum class SwipeDirection { BOTH, LEFT, RIGHT, NONE }

Next, wrappers around the existing onTouchEvent and onInterceptTouchEvent functions have to be added, so any attempts to move between pages can be checked before being acted on:

    override fun onTouchEvent(event: MotionEvent): Boolean {
        return if (this.isSwipeAllowed(event)) {
            super.onTouchEvent(event)
        } else false
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        return if (this.isSwipeAllowed(event)) {
            super.onInterceptTouchEvent(event)
        } else false
    }

Now, the isSwipeAllowed function from the previous wrappers has to be implemented, returning a boolean. If the permitted direction is BOTH, true can be returned instantly, and the same for NONE and returning false.

When the MotionEvent.ACTION_DOWN is fired, the initialXValue is updated, so we know where the swipe started. When any subsequent MotionEvent.ACTION_MOVE event occurs, the initialXValue and swipe event’s x can be used to calculate which way the user is swiping. The function can then return whether or not the swipe event is in a permitted direction.

    private fun isSwipeAllowed(event: MotionEvent): Boolean {
        if (this.direction === SwipeDirection.BOTH) {
            return true
        } else if (direction === SwipeDirection.NONE) {
            return false
        }

        if (event.action == MotionEvent.ACTION_DOWN) {
            initialXValue = event.x
            return true
        }

        if (event.action == MotionEvent.ACTION_MOVE) {
            try {
                val diffX = 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 (e: Exception) {
                e.printStackTrace()
            }
        }
        return true
    }

Finally, a simple function for setting the permitted swipe direction is added, and then the core of the

Using LockableViewPager

To use the new LockableViewPager, just set your desired swipe direction during onCreate / onViewCreated:

pager.setAllowedSwipeDirection(SwipeDirection.LEFT)

Example use case: unskippable pages

This code was originally used for inviting other users to an account. In this use case, some users were required (e.g. couldn’t be skipped), whereas others were optional (e.g. could be skipped). As such, the required users were displayed first, and had to be passed by completing an invite process which then programmatically moved to the next page. Required users could not be swiped away, but the subsequent optional users could be swiped between freely.

First, during the onCreate / onViewCreated, a custom page change listener is set using pager.addOnPageChangeListener(pageChangeListener()) so that swipe logic can be updated whenever a new page is navigated to. This listener is defined as:

    private fun pageChangeListener(): ViewPager.SimpleOnPageChangeListener =
        object : ViewPager.SimpleOnPageChangeListener() {
            override fun onPageSelected(position: Int) {
                setSwipeability()
            }
        }    

setSwipeability is just a wrapper around pager.setAllowedSwipeDirection(getSwipeDirection(pager)), which calls the main logic getSwipeDirection.

First, if the current user is required, no swiping is permitted:

        if (isDriverRequired(pager.currentItem)) {
            return SwipeDirection.NONE
        }

Next, various useful but simple values are calculated, to ensure there are no unreadably complicated boolean logic statements. The number of pages in the LockableViewPager is used extensively, and each statement builds on the last to avoid repeated logic.

        val isFirstUser = pager.currentItem == 0
        val isLastUser = pager.currentItem == pager.adapter!!.count - 1
        val isUserToLeft = !isFirstUser && pager.currentItem > 0
        val isUserToRight = !isLastUser && pager.currentItem < pager.adapter!!.count - 1
        val isOptionalUserOnLeft = isUserToLeft && !isUserRequired(pager.currentItem - 1)
        val isOptionalUserOnRight = isUserToRight && !isUserRequired(pager.currentItem + 1)

Now that all the information required to calculate the permitted swipe directions has been calculated, the actual end logic is extremely simple:

        if (isOptionalUserOnLeft && isOptionalUserOnRight) {
            return SwipeDirection.BOTH
        } else if (isOptionalUserOnLeft) {
            return SwipeDirection.LEFT
        } else if (isOptionalUserOnRight) {
            return SwipeDirection.RIGHT
        } else {
            return SwipeDirection.NONE
        }

Conclusion

Whilst the initial idea of having varying swipe options on a per-page basis seems simple, the default ViewPager has no capabilities for this. Luckily, the extension described in this post adds the functionality in a very easy to use way, and has no noticeable performance impact.

Further improvements would be adding an optional “bounce” animation when trying to navigate in a non-permitted direction, instead of just ignoring the swipe. The getSwipeDirection function could also be improved by reducing the amount of if statements and distinct boolean statements, albeit at a risk of decreased readability.

As mentioned before, all of this code is available as a Gist. Additionally, the core locking idea is originally from andre719mv‘s answer on StackOverflow.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s