SwipeRefreshLayout + ViewPager, ограничивать только горизонтальную прокрутку?

Я реализовал SwipeRefreshLayout и ViewPager в своем приложении, но есть большая проблема: всякий раз, когда я собираюсь прокручивать влево/вправо, чтобы переключаться между страницами, прокрутка слишком чувствительна. Небольшое прокручивание приведет к обновлению 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>

любая помощь будет оценена, спасибо

Ответ 1

Решилось очень просто, не расширив ничего

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

работает как шарм

Ответ 2

Я не уверен, что у вас все еще есть эта проблема, но приложение ввода-вывода Google 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);
    }
}

Я использовал то же самое и хорошо работал.

EDIT: используйте addOnPageChangeListener() вместо setOnPageChangeListener().

Ответ 3

Я встретил вашу проблему. Настройка 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);
}

Смотрите ссылку: ссылка

Ответ 4

Я основывал это на предыдущем ответе, но нашел, что это работает немного лучше. Движение начинается с события 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;
    }
});

Ответ 5

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

Самый простой способ решить эту проблему - просто скопировать класс из библиотеки поддержки в ваш проект и удалить переопределение метода. ViewGroup реализация использует внутреннее состояние для обработки onInterceptTouchEvent(), поэтому вы не можете просто снова переопределить метод и продублировать его. Если вы действительно хотите переопределить реализацию библиотеки поддержки, тогда вам придется настраивать пользовательский флаг при вызовах requestDisallowInterceptTouchEvent() и переопределять поведение onInterceptTouchEvent() и onTouchEvent() (или, возможно, взломать canChildScrollUp()) на основе этого.

Ответ 6

Существует одна проблема с решением nhasan:

Если горизонтальное прокручивание, вызывающее вызов setEnabled(false) на SwipeRefreshLayout в OnPageChangeListener, происходит, когда SwipeRefreshLayout уже распознал Pull-to-Reload, но еще не вызвал обратный вызов уведомления, анимация исчезает, но внутреннее состояние SwipeRefreshLayout остается на "освежающем" навсегда, поскольку вызывающие обратные вызовы не вызываются, что может reset состояние. С точки зрения пользователя это означает, что Pull-to-Reload больше не работает, поскольку все жесты тяги не распознаются.

Проблема заключается в том, что вызов disable(false) удаляет анимацию счетчика, и вызванный обратный вызов уведомления вызывается из метода onAnimationEnd для внутреннего AnimationListener для этого счетчика, который указан таким образом.

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

Решение исправить это - переопределить метод 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;
    }
}

Используйте MySwipeRefreshLayout в своем макете - файле и измените код в решении mhasan на

...

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

...

Ответ 7

Может быть проблема с ответом @huu duy, когда ViewPager помещается в вертикально прокручиваемый контейнер, который, в свою очередь, помещается в 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;
}

Смотрите мой пример проекта на Github.

Ответ 8

Я нашел решение для 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
}

Это работает как шарм для меня.