98

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

我想对水平滑动开始的时间设置一个限制,然后只强制水平直到滑动结束。换句话说,我想在手指水平移动时取消垂直滑动。

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

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

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:

<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>

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

4

9 回答 9

164

我不确定你是否还有这个问题,但谷歌 I/O 应用程序 iosched 解决了这个问题:

    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()。

于 2015-04-29T14:30:52.133 回答
37

非常简单地解决了没有扩展任何东西

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

像魅力一样工作

于 2014-09-23T10:03:14.830 回答
22

我遇到了你的问题。自定义 SwipeRefreshLayout 可以解决问题。

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

请参阅参考:链接

于 2015-10-19T05:43:31.803 回答
12

我将此基于先前的答案,但发现此方法效果更好。根据我的经验,动作以 ACTION_MOVE 事件开始,以 ACTION_UP 或 ACTION_CANCEL 结束。

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;
    }
});
于 2015-01-14T02:02:10.407 回答
9

出于某种只有他们才知道的原因,支持库开发团队认为从 的子布局中强行拦截所有垂直拖动运动事件是合适SwipeRefreshLayout的,即使孩子特别请求事件的所有权也是如此。他们唯一检查的是它的主要孩子的垂直滚动状态为零(如果它的孩子是垂直可滚动的)。该requestDisallowInterceptTouchEvent()方法已被一个空的主体覆盖,并且(不是这样)有启发性的注释“Nope”。

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

于 2014-09-22T21:28:48.280 回答
3

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

/**
 * 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
}

它对我来说就像一个魅力。

于 2019-06-22T06:43:04.850 回答
3

nhasan的解决方案有一个问题:

如果在已经识别到 Pull-to-Reload 但尚未调用通知回调时触发调用 in 的水平滑动,则动画消失但内部状态永远保持setEnabled(false)SwipeRefreshLayout刷新”为 no调用可以重置状态的通知回调。从用户的角度来看,这意味着 Pull-to-Reload 不再起作用,因为所有的拉动手势都无法识别。OnPageChangeListenerSwipeRefreshLayoutSwipeRefreshLayout

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

诚然,我们的测试人员用最快的手指引发了这种情况,但在现实场景中也可能偶尔发生。

解决此问题的解决方案是覆盖以下onInterceptTouchEvent方法SwipeRefreshLayout

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 解决方案中的代码更改为

...

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

...
于 2017-03-30T14:27:35.117 回答
1

2020-10-17

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

如果您已从 迁移ViewPagerViewPager2,请使用

registerOnPageChangeCallback监听滚动事件的方法

mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);
        swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
    }
}); 
于 2020-10-17T15:20:56.737 回答
0

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

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

于 2018-10-15T08:37:34.217 回答