Search code examples
javaandroidandroid-viewpagerswipetouch-event

Disable swipe on some fragments in ViewPager


I have a ViewPager that can disable or enable swipe touches:

public class ConfigurablePager extends ViewPager {

    private final AtomicBoolean touchesAllowed = new AtomicBoolean();

    ...

    private boolean touchesAllowed() {
        return touchesAllowed.get();
    }

    public void enableTouches() {
        touchesAllowed.set(true);
    }

    public void disableTouches() {
        touchesAllowed.set(false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return touchesAllowed() && super.onTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return touchesAllowed() && super.onInterceptTouchEvent(ev);
    }
}

Some fragments can be swiped but other can't. Pager adapter aware of swipe behaviour for each fragment. This behaviour can be changed in ViewPager.OnPageChangeListener:

@Override
public void onPageSelected(int position) {
    if (adapter.isTouchesAllowed(position)) {
        views.pager.enableTouches();
    } else {
        views.pager.disableTouches();
    }
}

The problem
Sometimes, when I swipe fragments very fast and click on tab for other fragment simultaneously viewpager can throw IllegalArgumentException:

FATAL EXCEPTION:
main java.lang.IllegalArgumentException: pointerIndex out of range
at android.view.MotionEvent.nativeGetAxisValue(Native Method)
at android.view.MotionEvent.getX(MotionEvent.java:1979)
at android.support.v4.view.MotionEventCompatEclair.getX(MotionEventCompatEclair.java:32)
at android.support.v4.view.MotionEventCompat$EclairMotionEventVersionImpl.getX(MotionEventCompat.java:110)
at android.support.v4.view.MotionEventCompat.getX(MotionEventCompat.java:462)
at android.support.v4.view.ViewPager.onTouchEvent(ViewPager.java:2080)
at com.test.debugpager.ConfigurablePager.onTouchEvent(ConfigurablePager.java:39)
at android.view.View.dispatchTouchEvent(View.java:7384)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2203)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1938)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2231)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1952)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2209)

It's happend because ViewPager save last pointerId and get inconsistent state (some touch events dropped by onInterceptTouchEvent) e.g. ACTION_MOVE with incorrect mActivePointerId from last touch event (see sources of ViewPager.java)

The question
Is it posible to disable swipe on some fragments in other way, maybe without overriding onInterceptTouchEvent?

ViewPager sources (onTouchEvent):

case MotionEvent.ACTION_MOVE:
    if (!mIsBeingDragged) {
        final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
        final float x = MotionEventCompat.getX(ev, pointerIndex);
        final float xDiff = Math.abs(x - mLastMotionX);
        final float y = MotionEventCompat.getY(ev, pointerIndex);
        final float yDiff = Math.abs(y - mLastMotionY);
        if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
        if (xDiff > mTouchSlop && xDiff > yDiff) {
            if (DEBUG) Log.v(TAG, "Starting drag!");
            mIsBeingDragged = true;
            requestParentDisallowInterceptTouchEvent(true);
            mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
            mInitialMotionX - mTouchSlop;
            mLastMotionY = y;
            setScrollState(SCROLL_STATE_DRAGGING);
            setScrollingCacheEnabled(true);

            // Disallow Parent Intercept, just in case
            ViewParent parent = getParent();
            if (parent != null) {
                parent.requestDisallowInterceptTouchEvent(true);
            }
        }
    }

Solution

  • Solved

    I've read intently android guide about gesture recognizing in a ViewGroup and analyzed ViewPager onTouchEvent sources. Here I recognize that ViewPager do swipe only for ACTION_MOVE event so we shouldn't call touch callbacks only for this action and we should obey base ViewGroup onInterceptTouchEvent result before calling base class onTouchEvent.

    According to this rules I changed my ViewPager code:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (touchesAllowed()) {
            return super.onInterceptTouchEvent(ev);
        } else {
            if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_MOVE) {
                // ignore move action
            } else {
                if (super.onInterceptTouchEvent(ev)) {
                    super.onTouchEvent(ev);
                }
            }
            return false;
        }
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (touchesAllowed()) {
            return super.onTouchEvent(ev);
        } else {
            return MotionEventCompat.getActionMasked(ev) != MotionEvent.ACTION_MOVE && super.onTouchEvent(ev);
        }
    }