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

96

Я реализовал 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>

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

user3896501
источник
Работает ли тот же сценарий, если один из ваших фрагментов внутри окна просмотра имеет SwipeRefreshLayout?
Zapnologica

Ответы:

161

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

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

РЕДАКТИРОВАТЬ: используйте addOnPageChangeListener () вместо setOnPageChangeListener ().

Нхасан
источник
3
Это лучший ответ, поскольку он учитывает состояние ViewPager. Это не предотвращает перетаскивание вниз, которое происходит на ViewPager, что явно демонстрирует намерение обновить.
Эндрю Галлаш
4
Лучший ответ, однако было бы неплохо опубликовать код для enableDisableSwipeRefresh (да, это очевидно из имени функции ... но, чтобы убедиться, что мне пришлось погуглить ...)
Грег Эннис
5
Работает отлично, но setOnPageChangeListener теперь обесценился. используйте вместо этого addOnPageChangeListener.
Йон Корнилов
@nhasan В недавнем обновлении этот ответ больше не работает. Установка для swiperefresh состояния enabled на false полностью удаляет swiperefresh, тогда как раньше, если состояние прокрутки viewpager изменялось во время обновления swiperefresh, он не удалял макет, но отключал бы его, сохраняя его в том же состоянии обновления, в котором он был раньше.
Майкл Тедла
2
Ссылка на исходный код для enableDisableSwipeRefresh в приложении google i / o: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo
37

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

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

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

user3896501
источник
Хорошо, но имейте в виду, что у вас будет такая же проблема для любой прокручиваемой, Viewкоторая может быть у вас внутри ViewPager, поскольку SwipeRefreshLayoutпозволяет даже вертикальную прокрутку только для ее дочернего элемента верхнего уровня (и в API ниже, чем ICS, только если это случайно ListView) .
corsair992
@ corsair992less Спасибо за советы
user3896501
@ corsair992 столкнулся с проблемой. У меня ViewPagerвнутри, SwipeRefrestLayoutи у ViewPager есть Listview! SwipeRefreshLayoutпозвольте мне прокрутить вниз, но при прокрутке вверх он запускает процесс обновления. Любое предложение?
Мухаммад Бабар
1
Можете ли вы рассказать немного больше о том, что такое mLayout?
desgraci
2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov
22

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

См. Ссылку : ссылка

хуу дуй
источник
Это лучшее решение для включения наклона при обнаружении.
nafsaka
Отличное решение +1
Tram Nguyen
12

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

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

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

корсар992
источник
Блин, это грубо. Мне очень жаль, что они этого не сделали. У меня есть список, который я хочу включить для обновления, а также возможность пролистывать элементы строки. То, как они создали SwipeRefreshLayout, делает это практически невозможным без некоторых безумных решений.
Джесси А. Моррис
3

Есть одна проблема с решением нхасана:

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

Проблема здесь в том, что при 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в своем макете - файл и измените код в решении мхасана на

...

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

...
Nantoka
источник
1
У меня тоже была такая же проблема, как и у вас. Только что измененное решение nhasan с помощью этого pastebin.com/XmfNsDKQ Примечание макета обновления смахивания не создает проблем при его обновлении, поэтому проверьте.
Амит
3

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

На меня это действует как оберег.

Алексей Шевелев
источник
0

Может возникнуть проблема с ответом @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 .

Станислав Перченко
источник
0

2020-10-17

минимальное дополнение к идеальному ответу @nhasan .

если вы перешли с ViewPagerна ViewPager2, используйте

registerOnPageChangeCallback метод прослушивания событий прокрутки

mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);
        swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
    }
}); 
Мистер AF
источник