HorizontalScrollView inside SwipeRefreshLayout HorizontalScrollView inside SwipeRefreshLayout android android

HorizontalScrollView inside SwipeRefreshLayout


I solved it by extending SwipeRefreshLayout and overriding its onInterceptTouchEvent. Inside, I calculate if the X distance the user has wandered is bigger than the touch slop. If it does, it means the user is swiping horizontally, therefor I return false which lets the child view (the HorizontalScrollView in this case) to get the touch event.

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);    }}


If you do not memorize the fact that you already declined the ACTION_MOVE event, you will eventually take it later if the user go back near your initial mPrevX.

Just add a boolean to memorize it.

public class CustomSwipeToRefresh extends SwipeRefreshLayout {    private int mTouchSlop;    private float mPrevX;    // Indicate if we've already declined the move event    private boolean mDeclined;    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();                mDeclined = false; // New action                break;            case MotionEvent.ACTION_MOVE:                final float eventX = event.getX();                float xDiff = Math.abs(eventX - mPrevX);                if (mDeclined || xDiff > mTouchSlop) {                    mDeclined = true; // Memorize                    return false;                }        }        return super.onInterceptTouchEvent(event);    }}


The solution proposed by Lior Iluz with overriding onInterceptTouchEvent() has a serious issue.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 of the original SwipeRefreshLayout from the support library to my project and re-wrote the onInterceptTouchEvent(). The new class name is TouchSafeSwipeRefreshLayout

private boolean mPendingActionDown;private float mInitialDownY;private float mInitialDownX;private boolean mGestureDeclined;@Overridepublic 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.