SwipeRefreshLayout + ViewPager,仅限水平滚动?

我已经在我的应用程序中实现了 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>

如有任何帮助,我将不胜感激,谢谢

33877 次浏览

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

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 override the 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.

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

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.

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

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

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

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

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

There is one problem with the solution of nhasan:

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

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

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 onInterceptTouchEvent method in SwipeRefreshLayout as follows:

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 MySwipeRefreshLayout in your Layout - File and change the code in the solution of mhasan to

...


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


...

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

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.

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

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

2020-10-17

a minimal addition to @nhasan perfect answer.

if you have migrated from ViewPager to ViewPager2, use

registerOnPageChangeCallback method for listening scroll events

mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
}
});