关于android:SwipeRefreshLayout + ViewPager,仅限制水平滚动吗?

SwipeRefreshLayout + ViewPager, limit horizontal scroll only?

我已经在我的应用程序中实现了SwipeRefreshLayoutViewPager,但有一个大麻烦:每当我要左右滑动以在页面之间切换时,滚动就太敏感了。 向下轻扫也会触发SwipeRefreshLayout刷新。

我想为开始水平滑动设置一个限制,然后仅在滑动结束之前强制水平滑动。 换句话说,当手指水平移动时,我想取消垂直滑动。

仅当ViewPager发生此问题,如果我向下滑动并触发SwipeRefreshLayout刷新功能(显示条),然后水平移动手指,它仍然仅允许垂直滑动。

我试图扩展ViewPager类,但是它根本不起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
    }

}

布局xml:

1
2
3
4
5
6
7
8
9
<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>

任何帮助,将不胜感激,谢谢


我不确定您是否仍然存在此问题,但是通过iosched的Google I / O应用程序可以解决此问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    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);
    }
}

我已经用过,并且效果很好。

编辑:使用addOnPageChangeListener()代替setOnPageChangeListener()。


解决非常简单,无需扩展任何内容

1
2
3
4
5
6
7
8
9
10
11
12
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;
    }
});

像魅力一样工作


我遇到了你的问题。自定义SwipeRefreshLayout将解决此问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
}

参见参考资料:链接


我以先前的答案为基础,但发现它的工作效果更好。根据我的经验,该动作以ACTION_MOVE事件开始,并以ACTION_UP或ACTION_CANCEL结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
    }
});


由于某些原因(只有他们最了解),支持库开发人员团队认为可以强制拦截SwipeRefreshLayout的子布局中的所有垂直拖动运动事件,即使当孩子特别要求该事件的所有权时也是如此。他们唯一要检查的是其主要子项的垂直滚动状态为零(如果其子项是可垂直滚动的)。 requestDisallowInterceptTouchEvent()方法已被一个空的正文覆盖,并且(不是)照亮注释为" Nope"。

解决此问题的最简单方法是将类从支持库复制到您的项目中,然后删除方法覆盖。 ViewGroup的实现使用内部状态来处理onInterceptTouchEvent(),因此您不能简单地再次重写该方法并复制它。如果您确实想覆盖支持库的实现,则必须在调用requestDisallowInterceptTouchEvent()时设置一个自定义标志,并基于此行为覆盖onInterceptTouchEvent()onTouchEvent()(或可能破解canChildScrollUp())行为。


我找到了ViewPager2的解决方案。我使用反射来降低阻力敏感性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 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
}

对我来说,它就像是一种魅力。


nhasan解决方案存在一个问题:

如果在SwipeRefreshLayout已识别出"重新加载"但尚未调用通知回调时发生了触发OnPageChangeListenerSwipeRefreshLayout上的SwipeRefreshLayout调用的setEnabled(false)的水平滑动,则动画会消失,但SwipeRefreshLayout的内部状态永远保持"刷新"状态,因为没有调用可以重置状态的通知回调。从用户角度看,这意味着"重新加载"不再起作用,因为无法识别所有拉动手势。

这里的问题是disable(false)调用删除了微调器的动画,并且从该微调器的内部AnimationListener的onAnimationEnd方法中调用了通知回调,这种方式不按顺序设置。

诚然,我们的测试人员用最快的手指来触发这种情况,但在现实情况下也可能偶尔发生。

解决此问题的一种方法是重写SwipeRefreshLayout中的onInterceptTouchEvent方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
    }
}

在布局-文件中使用MySwipeRefreshLayout并将mhasan解决方案中的代码更改为

1
2
3
4
5
6
7
8
...

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

...


2020-10-17

@nhasan完美答案的最小补充。

如果您已从ViewPager迁移到ViewPager2,请使用

registerOnPageChangeCallback方法,用于侦听滚动事件

1
2
3
4
5
6
7
mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);
        swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
    }
});

将ViewPager放在可垂直滚动的容器中时,@ huu duy答案可能有问题,而该容器又可以放在SwiprRefreshLayout中
如果内容可滚动容器未完全向上滚动,则可能无法以相同的向上滚动手势激活刷卡刷新。
确实,当您开始滚动内部容器并无意间水平移动手指而不是mTouchSlop时(默认为8dp),
建议的CustomSwipeToRefresh拒绝此手势。因此,用户必须再次尝试开始刷新。
对于用户来说,这可能看起来很奇怪。
我从支持库中将原始SwipeRefreshLayout的源代码提取到了我的项目中,并重新编写了onInterceptTouchEvent()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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;
}

请参阅我在Github上的示例项目。