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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25978462/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 09:53:07  来源:igfitidea点击:

SwipeRefreshLayout + ViewPager, limit horizontal scroll only?

androidandroid-viewpagergestureswiperefreshlayout

提问by user3896501

I've implemented SwipeRefreshLayoutand ViewPagerin 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 SwipeRefreshLayoutrefresh too.

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

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 SwipeRefreshLayoutrefresh function is triggered (the bar is shown) and then I move my finger horizontally, it still only allows vertical swipes.

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

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

我试图扩展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;
    }

}

Layout xml:

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

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

采纳答案by user3896501

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

像魅力一样工作

回答by nhasan

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

我不确定您是否仍然有这个问题,但 Google 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);
    }
}

I have used the same and works quite well.

我已经使用了相同的并且效果很好。

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

编辑:使用 addOnPageChangeListener() 而不是 setOnPageChangeListener()。

回答by huu duy

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

我遇到了你的问题。自定义 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);
}

See the ref: link

请参阅参考:链接

回答by Sean Abraham

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.

我基于以前的答案,但发现它的效果更好。根据我的经验,动作以 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;
    }
});

回答by corsair992

For some reason best known only to them, the support library developer team saw fit to forcefullyintercept 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".

出于某种只有他们最了解的原因,支持库开发团队认为适合从的子布局中强行拦截所有垂直拖动运动事件SwipeRefreshLayout,即使是在孩子明确请求事件所有权时也是如此。他们唯一检查的是它的主要孩子的垂直滚动状态为零(如果它的孩子是垂直滚动的)。该requestDisallowInterceptTouchEvent()方法已被一个空的主体和(并非如此)启发性的评论“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 overridethe 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.

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

回答by Alex Shevelev

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

我找到了 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
}

It works like a charm for me.

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

回答by Nantoka

There is one problem with the solution of nhasan:

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

If the horizontal swipe that triggers the setEnabled(false)call on the SwipeRefreshLayoutin the OnPageChangeListenerhappens when the SwipeRefreshLayouthas already recognized a Pull-to-Reload but has not yet called the notification callback, the animation disappears but the internal state of the SwipeRefreshLayoutstays 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.

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

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

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

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 onInterceptTouchEventmethod in SwipeRefreshLayoutas follows:

解决此问题的解决方案是按如下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;
    }
}

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

使用MySwipeRefreshLayout你的布局-文件并更改mhasan到解决方案的代码

...

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

...

回答by Stanislav Perchenko

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

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

See my example project on Github.

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