SwipeRefreshLayout + ViewPager,仅限制水平滚动吗?


94

我已经实现了,SwipeRefreshLayout并且ViewPager在我的应用程序中,但是有一个大麻烦:每当我要左右滑动以在页面之间切换时,滚动就太敏感了。稍微向下滑动也会触发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>

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


如果您在viewpager中的一个片段中有一个片段,是否可以使用相同的方案SwipeRefreshLayout
Zapnologica 2015年

Answers:


160

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

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


3
这是最佳答案,因为它考虑了ViewPager的状态。它不会阻止源自ViewPager的向下拖动,这显然表明了刷新的意图。
Andrew Gallasch 2015年

4
最佳答案,但是将代码发布到enableDisableSwipeRefresh(是的,从函数名称中可以明显看出。但是请确保我必须用Google搜索...)
Greg Ennis 2015年

5
完美运行,但是setOnPageChangeListener现在已贬值。请改用addOnPageChangeListener。
Yon Kornilov

@nhasan截至最近的更新,此答案不再起作用。将swiperefresh的启用状态设置为false会完全删除swiperefresh,而如果在swiperefresh刷新时viewpager的滚动状态发生更改,则它不会删除布局,但会禁用布局,同时将其保持在与以前相同的刷新状态。
迈克尔·泰德拉

2
链接到google i / o应用程序中“ enableDisableSwipeRefresh”的源代码:android.googlesource.com/platform/external/iosched
HEAD

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

像魅力一样工作


好的,但是请记住,您可能在中有任何可滚动的对象View,也会遇到同样的问题ViewPager,因为SwipeRefreshLayout甚至只允许对其顶层子对象进行垂直滚动(并且仅在低于ICS的API中碰巧是ListView) 。
corsair992 2014年

@ corsair992less感谢您的提示
user3896501 2014年

@ corsair992面临此问题。我有ViewPager里面SwipeRefrestLayout,而ViewPager有ListviewSwipeRefreshLayout让我向下滚动,但向上滚动会触发刷新进度。有什么建议吗?
穆罕默德·巴巴尔

1
您可以进一步开发什么是mLayout吗?
desgraci

2
viewPager.setOnTouchListener {_,event-> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov

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

参见参考资料:链接


这是最好的解决方案,将斜率包括在检测中。
nafsaka

很棒的解决方案+1
电车阮

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

Thnxx解决方案
Hitesh Kushwah

9

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

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


伙计,这很粗糙。我真的希望他们不会那样做。我有一个列表,我希望启用拉动刷新和滑动行项目的功能。他们创建SwipeRefreshLayout的方式几乎可以避免一些疯狂的工作。
杰西·莫里斯

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
}

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


2

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

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

这里的问题是该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);
}

...

1
我也有和你一样的问题。刚使用此pastebin.com/XmfNsDKQ修改了nhasan的解决方案注意,刷新时刷新刷新布局不会产生问题,因此这就是检查的原因。
阿米特·贾扬

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上的示例项目。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.