99

I've implemented SwipeRefreshLayout and ViewPager in my app but there is a big trouble: whenever I'm going to swipe left / right to switch between pages the scrolling is too sensitive. A little swipe down will trigger the SwipeRefreshLayout refresh too.

I want to set a limit to when horizontal swipe starts, then force horizontal only until swiping is over. In other words, I want to cancel vertical swipping when finger is moving horizontally.

This problem only occurs on ViewPager, if I swipe down and SwipeRefreshLayout refresh function is triggered (the bar is shown) and then I move my finger horizontally, it still only allows vertical swipes.

I've tried to extend the ViewPager class but it isn't working at all:

public class CustomViewPager extends ViewPager {

    public CustomViewPager(Context ctx, AttributeSet attrs) {
        super(ctx, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean in = super.onInterceptTouchEvent(ev);
        if (in) {
            getParent().requestDisallowInterceptTouchEvent(true);
            this.requestDisallowInterceptTouchEvent(true);
        }
        return false;
    }

}

Layout xml:

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/viewTopic"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.myapp.listloader.foundation.CustomViewPager
        android:id="@+id/topicViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>

any help would be appreciated, thanks

Juan José Melero Gómez
  • 2,742
  • 2
  • 19
  • 36
user3896501
  • 2,987
  • 1
  • 22
  • 25

9 Answers9

167

I am not sure if you still have this issue but Google I/O app iosched solves this problem thusly:

    viewPager.addOnPageChangeListener( new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled( int position, float v, int i1 ) {
        }

        @Override
        public void onPageSelected( int position ) {
        }

        @Override
        public void onPageScrollStateChanged( int state ) {
            enableDisableSwipeRefresh( state == ViewPager.SCROLL_STATE_IDLE );
        }
    } );


private void enableDisableSwipeRefresh(boolean enable) {
    if (swipeContainer != null) {
            swipeContainer.setEnabled(enable);
    }
}

I have used the same and works quite well.

EDIT: Use addOnPageChangeListener() instead of setOnPageChangeListener().

Phil
  • 4,730
  • 1
  • 41
  • 39
nhasan
  • 3,634
  • 3
  • 13
  • 5
  • 3
    This is the best answer because it takes account of the state of the ViewPager. It doesn't prevent a drag-downwards that originates on the ViewPager, which is clearly demonstrating an intention to refresh. – Andrew G Aug 10 '15 at 08:14
  • 4
    Best answer, however it might be nice to post the code to enableDisableSwipeRefresh (yes it is obvious from the function name.. but to be sure I had to google it...) – Greg Ennis Dec 29 '15 at 02:54
  • 5
    Works perfectly, but setOnPageChangeListener is depreciated now. use addOnPageChangeListener instead. – Yon Kornilov Feb 21 '16 at 03:42
  • 1
    @nhasan As of a recent update this answer doesn't work anymore. Setting swiperefresh's enabled state to false removes the swiperefresh entirely whereas before if the viewpager's scroll state changed while the swiperefresh is refreshing it wouldn't remove the layout, but would disable it while keeping it in the same refresh state it was in before. – Michael Tedla Oct 21 '16 at 10:40
  • 2
    Link to the source code for 'enableDisableSwipeRefresh' in the google i/o app: https://android.googlesource.com/platform/external/iosched/+/HEAD/android/src/main/java/com/google/samples/apps/iosched/ui/BaseActivity.java#1486 – jpardogo Nov 11 '16 at 11:39
  • This in Kotlin + RxBinding: ```pageScrollStateChanges(pager) .map { it == ViewPager.SCROLL_STATE_IDLE } .subscribe { swipeRefreshLayout.isEnabled = it }``` – kotucz Feb 05 '18 at 16:29
  • Worked perfectly in Kotlin. This is how we can write in Kotlin : private fun enableDisableSwipeRefresh(enable: Boolean) { if (this.mSwipeRefreshLayout != null) { mSwipeRefreshLayout.isEnabled = enable } } – Archa Feb 18 '19 at 01:10
  • Also works perfectly with ViewPager2. Use `registerOnPageChangeCallback()` instead of `addOnPageChangeListener()` in such case – Ivan Syabro May 14 '20 at 12:33
37

Solved very simply without extending anything

mPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        mLayout.setEnabled(false);
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                mLayout.setEnabled(true);
                break;
        }
        return false;
    }
});

work like a charm

user3896501
  • 2,987
  • 1
  • 22
  • 25
  • Ok, but keep in mind that you will have this same issue for _any_ scrollable `View` that you might have inside the `ViewPager`, as `SwipeRefreshLayout` allows even vertical scrolling only for it's top-level child (and in APIs lower than ICS only if it happens to be a `ListView`). – corsair992 Sep 23 '14 at 12:12
  • @corsair992 facing the issue. I have `ViewPager` inside `SwipeRefrestLayout` and the ViewPager has `Listview`! `SwipeRefreshLayout` let me scroll down but on scroll up it triggers the refresh progress. Any suggestion? – Muhammad Babar Aug 05 '15 at 08:15
  • 1
    can you develop a little more about what is mLayout? – desgraci Dec 27 '16 at 20:05
  • could you please explain what mLayout is? I am facing this issue right now :O – CantThinkOfAnything Jun 20 '17 at 12:20
  • 2
    viewPager.setOnTouchListener { _, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false } – Axrorxo'ja Yodgorov Mar 13 '18 at 03:30
23

I've met your problem. Customize the SwipeRefreshLayout would solve the problem.

public class CustomSwipeToRefresh extends SwipeRefreshLayout {

private int mTouchSlop;
private float mPrevX;

public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
    super(context, attrs);

    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPrevX = MotionEvent.obtain(event).getX();
            break;

        case MotionEvent.ACTION_MOVE:
            final float eventX = event.getX();
            float xDiff = Math.abs(eventX - mPrevX);

            if (xDiff > mTouchSlop) {
                return false;
            }
    }

    return super.onInterceptTouchEvent(event);
}

See the ref: link

Community
  • 1
  • 1
huu duy
  • 2,049
  • 21
  • 31
12

I based this off a previous answer but found this to work a bit better. The motion starts with an ACTION_MOVE event and ends in either ACTION_UP or ACTION_CANCEL in my experience.

mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mSwipeRefreshLayout.setEnabled(false);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeRefreshLayout.setEnabled(true);
                break;
        }
        return false;
    }
});
Sean Abraham
  • 191
  • 2
  • 3
9

For some reason best known only to them, the support library developer team saw fit to forcefully intercept all vertical drag motion events from SwipeRefreshLayout's child layout, even when a child specifically requests ownership of the event. The only thing they check for is that vertical scroll state of it's main child is at zero (in the case that it's child is vertically scrollable). The requestDisallowInterceptTouchEvent() method has been overridden with an empty body, and the (not so) illuminating comment "Nope".

The easiest way to solve this issue would be to just copy the class from the support library into your project and remove the method override. ViewGroup's implementation uses internal state for handling onInterceptTouchEvent(), so you cannot simply override the method again and duplicate it. If you really want to override the support library implementation, then you will have to set up a custom flag upon calls to requestDisallowInterceptTouchEvent(), and override onInterceptTouchEvent() and onTouchEvent() (or possibly hack canChildScrollUp()) behavior based on that.

corsair992
  • 3,050
  • 22
  • 33
  • Man, that's rough. I really wish they wouldn't have done that. I have a list that I want to enable pull to refresh and the ability to swipe the row items. The way they've created the SwipeRefreshLayout makes that nearly impossible without some crazy work arounds. – Jessie A. Morris Jun 01 '15 at 20:49
3

There is one problem with the solution of nhasan:

If the horizontal swipe that triggers the setEnabled(false) call on the SwipeRefreshLayout in the OnPageChangeListener happens when the SwipeRefreshLayout has already recognized a Pull-to-Reload but has not yet called the notification callback, the animation disappears but the internal state of the SwipeRefreshLayout stays on "refreshing" forever as no notification callbacks are called that could reset the state. From a user perspective this means that Pull-to-Reload is not working anymore as all pull gestures are not recognized.

The problem here is that the disable(false) call removes the animation of the spinner and the notification callback is called from the onAnimationEnd method of an internal AnimationListener for that spinner which is set out of order that way.

It took admittedly our tester with the fastest fingers to provoke this situation but it can happen once in a while in realistic scenarios as well.

A solution to fix this is to override the onInterceptTouchEvent method in SwipeRefreshLayout as follows:

public class MySwipeRefreshLayout extends SwipeRefreshLayout {

    private boolean paused;

    public MySwipeRefreshLayout(Context context) {
        super(context);
        setColorScheme();
    }

    public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setColorScheme();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (paused) {
            return false;
        } else {
            return super.onInterceptTouchEvent(ev);
        }
    }

    public void setPaused(boolean paused) {
        this.paused = paused;
    }
}

Use the MySwipeRefreshLayout in your Layout - File and change the code in the solution of mhasan to

...

@Override
public void onPageScrollStateChanged(int state) {
    swipeRefreshLayout.setPaused(state != ViewPager.SCROLL_STATE_IDLE);
}

...
Nantoka
  • 4,174
  • 1
  • 33
  • 36
  • 1
    I also had the same problem as yours. Just modified nhasan's solution with this https://pastebin.com/XmfNsDKQ Note swipe refresh layout does not create problem when its refreshing, so that's why the check. – Amit Jayant Jul 07 '17 at 05:50
3

I've found a solution for ViewPager2. I use reflection for reducing drag sensitivity like this:

/**
 * Reduces drag sensitivity of [ViewPager2] widget
 */
fun ViewPager2.reduceDragSensitivity() {
    val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
    recyclerViewField.isAccessible = true
    val recyclerView = recyclerViewField.get(this) as RecyclerView

    val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
    touchSlopField.isAccessible = true
    val touchSlop = touchSlopField.get(recyclerView) as Int
    touchSlopField.set(recyclerView, touchSlop*8)       // "8" was obtained experimentally
}

It works like a charm for me.

Alex Shevelev
  • 681
  • 7
  • 14
1

2020-10-17

a minimal addition to @nhasan perfect answer.

if you have migrated from ViewPager to ViewPager2, use

registerOnPageChangeCallback method for listening scroll events

mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);
        swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
    }
}); 
A Farmanbar
  • 4,381
  • 5
  • 24
  • 42
0

There could be a problem with @huu duy answer when the ViewPager is placed in a vertically-scrollable container which, in turn, is placed in the SwiprRefreshLayout If the content scrollable container is not fully scrolled-up, then it may be not possible to activate swipe-to-refresh in the same scroll-up gesture. Indeed, when you start scrolling the inner container and move finger horizontally more then mTouchSlop unintentionally (which is 8dp by default), the proposed CustomSwipeToRefresh declines this gesture. So a user has to try once more to start refreshing. This may look odd for the user. I extracted the source code f the original SwipeRefreshLayout from the support library to my project and re-wrote the onInterceptTouchEvent().

private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
private boolean mPendingActionDown;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();
    final int action = ev.getActionMasked();
    int pointerIndex;

    if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
        mReturningToStart = false;
    }

    if (!isEnabled() || mReturningToStart || mRefreshing ) {
        // Fail fast if we're not in a state where a swipe is possible
        if (D) Log.e(LOG_TAG, "Fail because of not enabled OR refreshing OR returning to start. "+motionEventToShortText(ev));
        return false;
    }

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
            mActivePointerId = ev.getPointerId(0);

            if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) >= 0) {

                if (mNestedScrollInProgress || canChildScrollUp()) {
                    if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. Set pending DOWN=true. "+motionEventToShortText(ev));
                    mPendingActionDown = true;
                } else {
                    mInitialDownX = ev.getX(pointerIndex);
                    mInitialDownY = ev.getY(pointerIndex);
                }
            }
            return false;

        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                if (D) Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                return false;
            } else if (mGestureDeclined) {
                if (D) Log.e(LOG_TAG, "Gesture was declined previously because of horizontal swipe");
                return false;
            } else if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) < 0) {
                return false;
            } else if (mNestedScrollInProgress || canChildScrollUp()) {
                if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. "+motionEventToShortText(ev));
                return false;
            } else if (mPendingActionDown) {
                // This is the 1-st Move after content stops scrolling.
                // Consider this Move as Down (a start of new gesture)
                if (D) Log.e(LOG_TAG, "Consider this move as down - setup initial X/Y."+motionEventToShortText(ev));
                mPendingActionDown = false;
                mInitialDownX = ev.getX(pointerIndex);
                mInitialDownY = ev.getY(pointerIndex);
                return false;
            } else if (Math.abs(ev.getX(pointerIndex) - mInitialDownX) > mTouchSlop) {
                mGestureDeclined = true;
                if (D) Log.e(LOG_TAG, "Decline gesture because of horizontal swipe");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            startDragging(y);
            if (!mIsBeingDragged) {
                if (D) Log.d(LOG_TAG, "Waiting for dY to start dragging. "+motionEventToShortText(ev));
            } else {
                if (D) Log.d(LOG_TAG, "Dragging started! "+motionEventToShortText(ev));
            }
            break;

        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mGestureDeclined = false;
            mPendingActionDown = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }

    return mIsBeingDragged;
}

See my example project on Github.