SwipeRefreshLayout + ViewPager, ograniczyć tylko przewijanie w poziomie?

96

Zaimplementowałem SwipeRefreshLayoutiw ViewPagermojej aplikacji, ale jest duży problem: za każdym razem, gdy zamierzam przesuwać palcem w lewo / w prawo, aby przełączać się między stronami, przewijanie jest zbyt czułe. Małe przesunięcie w dół również spowoduje SwipeRefreshLayoutodświeżenie.

Chcę ustawić limit czasu, w którym zaczyna się przesuwanie w poziomie, a następnie wymuszać ustawienie w poziomie tylko do zakończenia przesuwania. Innymi słowy, chcę anulować przesuwanie w pionie, gdy palec porusza się w poziomie.

Ten problem występuje tylko wtedy ViewPager, gdy przeciągnę w dół i SwipeRefreshLayouturuchomiona zostanie funkcja odświeżania (wyświetlany jest pasek), a następnie poruszę palcem w poziomie, nadal umożliwia to tylko pionowe przesunięcia.

Próbowałem przedłużyć ViewPagerklasę, ale w ogóle nie działa:

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

}

Układ 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>

każda pomoc byłaby doceniona, dzięki

user3896501
źródło
Czy ten sam scenariusz działa, jeśli jeden z twoich fragmentów w podglądzie ma SwipeRefreshLayout?
Zapnologica

Odpowiedzi:

160

Nie jestem pewien, czy nadal masz ten problem, ale aplikacja Google I / O iosched rozwiązuje ten problem w ten sposób:

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

Użyłem tego samego i działa całkiem dobrze.

EDYCJA: użyj addOnPageChangeListener () zamiast setOnPageChangeListener ().

nhasan
źródło
3
To najlepsza odpowiedź, ponieważ bierze pod uwagę stan ViewPagera. Nie zapobiega przeciągnięciu w dół, które pochodzi z ViewPager, co wyraźnie pokazuje zamiar odświeżenia.
Andrew Gallasch
4
Najlepsza odpowiedź, jednak fajnie byłoby opublikować kod do enableDisableSwipeRefresh (tak, to oczywiste z nazwy funkcji ... ale dla pewności musiałem to wygooglować ...)
Greg Ennis
5
Działa idealnie, ale setOnPageChangeListener jest teraz amortyzowany. zamiast tego użyj addOnPageChangeListener.
Yon Kornilov,
@nhasan Od niedawnej aktualizacji ta odpowiedź już nie działa. Ustawienie włączonego stanu swiperefresh na false całkowicie usuwa swiperefresh, podczas gdy wcześniej, jeśli stan przewijania podglądu zmienił się podczas odświeżania swiperefresh, nie usuwałoby układu, ale wyłączyłby go, zachowując ten sam stan odświeżania, w którym był wcześniej.
Michael Tedla,
2
Link do kodu źródłowego funkcji „enableDisableSwipeRefresh” w aplikacji Google I / O: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo
37

Rozwiązany bardzo prosto, bez rozszerzania czegokolwiek

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

działają jak urok

user3896501
źródło
Ok, ale pamiętaj, że będziesz mieć ten sam problem z każdym przewijaniem, Viewktóre możesz mieć wewnątrz ViewPager, ponieważ SwipeRefreshLayoutpozwala nawet na przewijanie w pionie tylko dla jego dziecka najwyższego poziomu (i w API niższych niż ICS tylko wtedy, gdy jest ListView) .
corsair992,
@ corsair992less Dzięki za wskazówki
user3896501
@ corsair992 w obliczu problemu. mamViewPager środku, SwipeRefrestLayouta ViewPager ma Listview! SwipeRefreshLayoutpozwól mi przewinąć w dół, ale po przewinięciu w górę wyzwala postęp odświeżania. Jakieś sugestie?
Muhammad Babar,
1
czy możesz dowiedzieć się więcej o tym, czym jest mLayout?
desgraci
2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov
22

Spotkałem twój problem. Dostosowanie SwipeRefreshLayout rozwiązałoby problem.

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

Zobacz ref: link

huu duy
źródło
Jest to najlepsze rozwiązanie, aby uwzględnić nachylenie podczas wykrywania.
nafsaka
Świetne rozwiązanie +1
Tramwaj Nguyen
12

Oparłem to na poprzedniej odpowiedzi, ale okazało się, że działa to trochę lepiej. Ruch zaczyna się od zdarzenia ACTION_MOVE i kończy się ACTION_UP lub ACTION_CANCEL z mojego doświadczenia.

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;
    }
});
Sean Abraham
źródło
Thnxx za rozwiązanie
Hitesh Kushwah
9

Z jakiegoś najlepiej znanego tylko im powodu zespół programistów biblioteki pomocy uznał za stosowne przechwycić na siłę wszystkie zdarzenia ruchu przeciągania w pionie z SwipeRefreshLayoutukładu podrzędnego, nawet jeśli dziecko wyraźnie żąda własności wydarzenia. Jedyną rzeczą, którą sprawdzają, jest to, że stan przewijania w pionie jego głównego elementu potomnego wynosi zero (w przypadku, gdy jego element podrzędny jest przewijalny w pionie). requestDisallowInterceptTouchEvent()Metoda została zastąpiona z pustym korpusem, a (nie tak) oświetlając komentarz „Nope”.

Najłatwiejszym sposobem rozwiązania tego problemu byłoby po prostu skopiowanie klasy z biblioteki pomocy technicznej do projektu i usunięcie nadpisania metody. ViewGroupImplementacja wykorzystuje stan wewnętrzny do obsługi onInterceptTouchEvent(), więc nie można po prostu ponownie przesłonić metody i powielić jej. Jeśli naprawdę chcesz przesłonić implementację biblioteki obsługi, będziesz musiał ustawić niestandardową flagę dla wywołań requestDisallowInterceptTouchEvent()i przesłonić onInterceptTouchEvent()i onTouchEvent()(lub być może włamać canChildScrollUp()) zachowanie na tej podstawie.

corsair992
źródło
Człowieku, to szorstkie. Naprawdę chciałbym, żeby tego nie zrobili. Mam listę, którą chcę włączyć ciągnięcie, aby odświeżyć i możliwość przesuwania elementów wiersza. Sposób, w jaki utworzyli SwipeRefreshLayout, sprawia, że ​​jest to prawie niemożliwe bez szalonej pracy.
Jessie A. Morris
3

Jest jeden problem z rozwiązaniem nhasan:

Jeśli poziome przesunięcie, które wyzwala setEnabled(false)wywołanie SwipeRefreshLayoutw, OnPageChangeListenerma miejsce, gdy urządzenie SwipeRefreshLayoutjuż rozpoznało polecenie Pull-to-Reload, ale nie wywołało jeszcze wywołania zwrotnego powiadomienia, animacja znika, ale stan wewnętrzny SwipeRefreshLayoutpozostaje „odświeżany” na zawsze, ponieważ nie wywoływane są powiadomienia zwrotne, które mogą zresetować stan. Z punktu widzenia użytkownika oznacza to, że przeciągnij, aby przeładować, nie działa już, ponieważ wszystkie gesty ściągania nie są rozpoznawane.

Problem polega na tym, że disable(false)wywołanie usuwa animację przędzarki, a wywołanie zwrotne powiadomienia jest wywoływane z onAnimationEndmetody wewnętrznej AnimationListener dla tego przędzarki, która jest ustawiona w ten sposób.

Trzeba przyznać, że nasz tester miał najszybsze palce, aby sprowokować taką sytuację, ale może się to zdarzyć raz na jakiś czas w realistycznych scenariuszach.

Rozwiązaniem tego problemu jest zastąpienie onInterceptTouchEventmetody w SwipeRefreshLayoutnastępujący sposób:

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

Użyj MySwipeRefreshLayoutw swoim układzie - plik i zmień kod w rozwiązaniu mhasan na

...

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

...
Nantoka
źródło
1
Ja też miałem ten sam problem co twój. Właśnie zmodyfikowane rozwiązanie nhasan za pomocą tego pastebin.com/XmfNsDKQ Uwaga układ odświeżania swipe nie stwarza problemu podczas jego odświeżania, dlatego właśnie sprawdzanie.
Amit Jayant,
3

Znalazłem rozwiązanie dla ViewPager2. Używam odbicia do zmniejszenia wrażliwości na przeciąganie w następujący sposób:

/**
 * 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
}

Dla mnie działa jak urok.

Alex Shevelev
źródło
0

Może wystąpić problem z odpowiedzią @huu duy, gdy ViewPager jest umieszczony w pojemniku przewijanym w pionie, który z kolei jest umieszczony w SwiprRefreshLayout. Jeśli przewijalny kontener treści nie jest w pełni przewijany w górę, może nie być możliwe aktywuj przeciągnij, aby odświeżyć tym samym gestem przewijania w górę. Rzeczywiście, kiedy zaczniesz przewijać wewnętrzny kontener i nieumyślnie przesuniesz palec w poziomie bardziej niż mTouchSlop (co jest domyślnie 8dp), proponowany CustomSwipeToRefresh odrzuca ten gest. Dlatego użytkownik musi jeszcze raz spróbować rozpocząć odświeżanie. Może to wyglądać dziwnie dla użytkownika. Wyodrębniłem kod źródłowy oryginalnego SwipeRefreshLayout z biblioteki wsparcia do mojego projektu i ponownie napisałem 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;
}

Zobacz mój przykładowy projekt na Github .

Stanislav Perchenko
źródło
0

2020-10-17

minimalny dodatek do idealnej odpowiedzi @nhasan .

jeśli migrowałeś z ViewPagerdo ViewPager2, użyj

registerOnPageChangeCallback metoda nasłuchiwania zdarzeń przewijania

mPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);
        swipe.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
    }
}); 
Panie AF
źródło