55

我在我的应用程序中实现了新SwipeRefreshLayout组件,它适用于任何垂直视图,ListView例如GridViewScrollView

它在水平视图中表现得非常糟糕,例如HorizontalScrollView. 当向右或向左滚动时,SwipeRefreshLayout视图缓存触摸,阻止HorizontalScrollView接收它并开始垂直滚动以执行刷新。

ScrollView我尝试解决这个问题,因为我之前用ViewPager内部解决了垂直问题,requestDisallowInterceptTouchEvent但它没有用。我还注意到这个方法在原始SwipeRefreshLayout类中被覆盖而没有返回超级。谷歌的开发者留下了评论“ //Nope.”:)

因为SwipeRefreshLayout组件相对较新,我找不到解决水平滚动问题的解决方案,同时仍然允许滑动刷新视图以跟踪和处理垂直滚动,所以我想我会分享我的解决方案,希望它能节省一个小时或两个。

4

5 回答 5

165

我通过扩展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);
    }
}
于 2014-06-02T08:07:02.383 回答
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);
    }
}
于 2014-06-27T13:34:56.477 回答
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上的示例项目。

于 2018-10-15T08:20:03.137 回答
0

如果您使用 Tim Roes EnhancedListView

看到这个问题。我对我非常有用,因为他们添加了一个功能,可以检测何时开始滑动以及何时结束滑动。

当滑动开始时,我禁用 SwipeRefreshLayout,当滑动完成时,我启用 swipeRefreshLayout。

于 2014-08-29T08:31:21.923 回答
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 }
于 2020-07-16T21:23:59.667 回答