我在我的应用程序中实现了新的SwipeRefreshLayout
组件,它适用于任何垂直视图,例如ListView
,GridView
和ScrollView
。
水平视图表现得非常糟糕,例如HorizontalScrollView
。
当向右或向左滚动时,SwipeRefreshLayout
视图会缓存触摸,阻止HorizontalScrollView
接收它并开始垂直滚动以执行刷新。
我尝试解决此问题,因为我之前使用ScrollView
使用ViewPager
来解决垂直requestDisallowInterceptTouchEvent
的问题,但它无效。我还注意到,在原始SwipeRefreshLayout
类中重写此方法而不返回super。谷歌的开发者留下了评论,而不是“//Nope.
”:)
因为SwipeRefreshLayout
组件相对较新,我找不到修复水平滚动问题的解决方案,同时仍允许滑动刷新视图以跟踪和处理垂直滚动,所以我想我会分享我的解决方案希望它会让一个人花一两个小时。
答案 0 :(得分:141)
我通过扩展SwipeRefreshLayout
并覆盖其onInterceptTouchEvent
来解决它。在里面,我计算用户徘徊的X距离是否大于触摸坡度。如果是,则表示用户正在水平滑动,因此我返回false
,让孩子查看(在这种情况下为HorizontalScrollView
)以获取触摸事件。
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);
}
}
答案 1 :(得分:27)
如果您没有记住已经拒绝了ACTION_MOVE事件的事实,那么如果用户回到您的初始mPrevX附近,您最终会在以后接受该事件。
只需添加一个布尔值来记住它。
public class CustomSwipeToRefresh extends SwipeRefreshLayout {
private int mTouchSlop;
private float mPrevX;
// Indicate if we've already declined the move event
private boolean mDeclined;
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();
mDeclined = false; // New action
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (mDeclined || xDiff > mTouchSlop) {
mDeclined = true; // Memorize
return false;
}
}
return super.onInterceptTouchEvent(event);
}
}
答案 2 :(得分:1)
Lior Iluz提出的覆盖onInterceptTouchEvent()的解决方案存在严重问题。 如果内容可滚动容器未完全向上滚动,则可能无法以相同的向上滚动手势激活刷卡刷新。 的确,当您开始滚动内部容器并无意间水平移动手指而不是 mTouchSlop 时(默认为8dp), 建议的CustomSwipeToRefresh拒绝此手势。因此,用户必须再次尝试开始刷新。对于用户来说,这可能看起来很奇怪。
我从支持库中提取了原始SwipeRefreshLayout的源代码到我的项目中,并重新编写了onInterceptTouchEvent()。新的类名称为 TouchSafeSwipeRefreshLayout
private boolean mPendingActionDown;
private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
@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上查看我的示例项目。
答案 3 :(得分:0)
如果您使用Tim Roes EnhancedListView
见issues。我对我非常有用,因为它们添加了一个功能,可以检测滑动开始时和滑动完成时的效果。
当滑动开始时,我禁用SwipeRefreshLayout,当滑动完成时,我可以使用swipeRefreshLayout。
答案 4 :(得分:0)
这是我所做的:
class HorizontalScrollViewWithDragListener
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : HorizontalScrollView(context, attrs) {
var draggingState: Boolean = false
set(value) {
if (field != value) {
field = value
listener?.invoke(value)
}
}
var listener: ((draggingState: Boolean) -> Unit)? = null
override fun onInterceptTouchEvent(ev: MotionEvent) =
super.onInterceptTouchEvent(ev)
.also { draggingState = it }
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent) =
super.onTouchEvent(ev)
.also {
if(ev.action == MotionEvent.ACTION_UP) {
draggingState = false
}
}
}
然后我只需在设置代码中执行此操作:
myScrollView.listener = { refreshView.isEnabled = !it }