HorizontalScrollView внутри SwipeRefreshLayout

Я реализовал новый компонент SwipeRefreshLayout в своем приложении и хорошо работает с любыми вертикальными представлениями, такими как ListView, GridView и ScrollView.

Это ведет себя очень плохо с горизонтальными представлениями, например HorizontalScrollView. При прокрутке вправо или влево представление SwipeRefreshLayout кэширует касание, предотвращает его получение HorizontalScrollView и начинает прокрутку по вертикали для выполнения обновления.

Я попытался решить эту проблему, поскольку ранее я решил проблемы с вертикальной ScrollView с ViewPager внутри, используя requestDisallowInterceptTouchEvent, но это не сработало. Я также заметил, что этот метод переопределен в исходном классе SwipeRefreshLayout, не возвращая супер. Вместо этого разработчик Google оставил комментарий "//Nope.":)

Поскольку компонент SwipeRefreshLayout является относительно новым, я не смог найти решение, которое устраняет проблему горизонтального прокрутки, в то же время позволяя прокручивать изображение, чтобы отслеживать и обрабатывать вертикальную прокрутку, поэтому я решил поделиться своим решением с надеждой он пощадит кого-то час или два.

Ответ 1

Я решил его, расширив 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);
    }
}

Ответ 2

Если вы не запомните тот факт, что вы уже отклонили событие 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);
    }
}

Ответ 3

Решение, предложенное Лиором Илузом с переопределением 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.

Ответ 4

Если вы используете Tim Roes EnhancedListView

Смотрите issues. Я был очень полезен для меня, потому что они добавляют функцию, которая обнаруживает, когда начинается салфетка и когда салфетки заканчиваются.

При запуске сабля я отключу SwipeRefreshLayout и, когда закончим салфетки, я могу использовать swipeRefreshLayout.