SwipeRefreshLayout + ViewPager, limitare solo lo scorrimento orizzontale?


94

L'ho implementato SwipeRefreshLayoute ViewPagernella mia app, ma c'è un grosso problema: ogni volta che scorro a sinistra / destra per passare da una pagina all'altra, lo scorrimento è troppo sensibile. Un piccolo scorrimento verso il basso attiverà anche l' SwipeRefreshLayoutaggiornamento.

Voglio impostare un limite all'avvio dello scorrimento orizzontale, quindi forzare l'orizzontale solo fino al termine dello scorrimento. In altre parole, desidero annullare lo scorrimento verticale quando il dito si sposta in orizzontale.

Questo problema si verifica solo su ViewPager, se scorro verso il basso e SwipeRefreshLayoutviene attivata la funzione di aggiornamento (viene visualizzata la barra) e poi sposto il dito in orizzontale, consente comunque solo lo scorrimento verticale.

Ho provato ad estendere la ViewPagerclasse ma non funziona affatto:

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

}

Layout 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>

qualsiasi aiuto sarebbe apprezzato, grazie


Lo stesso scenario funziona se uno dei tuoi frammenti all'interno del viewpager ha un SwipeRefreshLayout?
Zapnologica

Risposte:


160

Non sono sicuro che tu abbia ancora questo problema, ma l'app Google I / O iosched risolve questo problema in questo modo:

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

Ho usato lo stesso e funziona abbastanza bene.

EDIT: Usa addOnPageChangeListener () invece di setOnPageChangeListener ().


3
Questa è la risposta migliore perché tiene conto dello stato del ViewPager. Non impedisce un trascinamento verso il basso che ha origine sul ViewPager, che sta chiaramente dimostrando l'intenzione di aggiornare.
Andrew Gallasch

4
Risposta migliore, tuttavia potrebbe essere bello pubblicare il codice per abilitare DisableSwipeRefresh (sì, è ovvio dal nome della funzione .. ma per essere sicuro che dovevo cercarlo su google ...)
Greg Ennis

5
Funziona perfettamente, ma setOnPageChangeListener è ora deprezzato. usa invece addOnPageChangeListener.
Yon Kornilov

@nhasan A partire da un recente aggiornamento questa risposta non funziona più. L'impostazione dello stato abilitato di swiperefresh su false rimuove completamente swiperefresh mentre prima se lo stato di scorrimento del viewpager cambiava mentre swiperefresh si aggiorna, non rimuoverebbe il layout, ma lo disabiliterebbe mantenendolo nello stesso stato di aggiornamento in cui si trovava prima.
Michael Tedla

2
Link al codice sorgente per "enableDisableSwipeRefresh" nell'app google i / o: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo

37

Risolto molto semplicemente senza estendere nulla

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

funziona come un fascino


Ok, ma tieni presente che avrai lo stesso problema per qualsiasi scorrevole Viewche potresti avere all'interno di ViewPager, in quanto SwipeRefreshLayoutconsente lo scorrimento anche verticale solo per il suo figlio di primo livello (e nelle API inferiori a ICS solo se capita di essere a ListView) .
corsair992

@ corsair992less Grazie per i tuoi suggerimenti
user3896501

@ corsair992 che affronta il problema. Ho ViewPagerdentro SwipeRefrestLayoute il ViewPager ha Listview! SwipeRefreshLayoutfammi scorrere verso il basso ma scorrendo verso l'alto attiva l'avanzamento dell'aggiornamento. Qualche suggerimento?
Muhammad Babar

1
puoi sviluppare un po 'di più su cosa è mLayout?
desgraci

2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov

22

Ho incontrato il tuo problema. Personalizzare lo SwipeRefreshLayout risolverebbe il problema.

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

Vedi il ref: link


Questa è la soluzione migliore per includere la pendenza nel rilevamento.
nafsaka

Ottima soluzione +1
Tram Nguyen

12

L'ho basato su una risposta precedente, ma ho scoperto che funziona un po 'meglio. Il movimento inizia con un evento ACTION_MOVE e termina con ACTION_UP o ACTION_CANCEL nella mia esperienza.

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 per la soluzione
Hitesh Kushwah

9

Per qualche motivo noto solo a loro, il team di sviluppo della libreria di supporto ha ritenuto opportuno intercettare con forza tutti gli eventi di movimento di trascinamento verticale dal SwipeRefreshLayoutlayout figlio di, anche quando un bambino richiede specificamente la proprietà dell'evento. L'unica cosa che controllano è che lo stato di scorrimento verticale del suo figlio principale sia a zero (nel caso in cui il figlio sia scorrevole verticalmente). Il requestDisallowInterceptTouchEvent()metodo è stato sovrascritto con un corpo vuoto e il commento (non così) illuminante "No".

Il modo più semplice per risolvere questo problema sarebbe semplicemente copiare la classe dalla libreria di supporto nel tuo progetto e rimuovere l'override del metodo. ViewGroupL'implementazione di utilizza lo stato interno per la gestione onInterceptTouchEvent(), quindi non è possibile sovrascrivere nuovamente il metodo e duplicarlo. Se vuoi davvero sovrascrivere l'implementazione della libreria di supporto, dovrai impostare un flag personalizzato sulle chiamate a requestDisallowInterceptTouchEvent(), e sovrascrivere onInterceptTouchEvent()e onTouchEvent()(o eventualmente hackerare canChildScrollUp()) il comportamento basato su quello.


Amico, è rozzo. Vorrei davvero che non lo avessero fatto. Ho un elenco che voglio abilitare per l'aggiornamento pull e la possibilità di scorrere gli elementi della riga. Il modo in cui hanno creato SwipeRefreshLayout lo rende quasi impossibile senza qualche folle lavoro intorno.
Jessie A. Morris

3

Ho trovato una soluzione per ViewPager2. Uso la riflessione per ridurre la sensibilità alla resistenza in questo modo:

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

Funziona come un fascino per me.


2

C'è un problema con la soluzione di nhasan:

Se lo swipe orizzontale che fa scattare la setEnabled(false)chiamata sul SwipeRefreshLayoutin OnPageChangeListeneravviene quando il SwipeRefreshLayoutha già riconosciuto un Pull-to-Reload ma non ha ancora chiamato il callback di notifica, l'animazione scompare ma lo stato interno delSwipeRefreshLayout rimane su "refreshing" per sempre come no vengono chiamati callback di notifica che potrebbero ripristinare lo stato. Dal punto di vista dell'utente, ciò significa che Pull-to-Reload non funziona più poiché tutti i gesti pull non vengono riconosciuti.

Il problema qui è che la disable(false)chiamata rimuove l'animazione dello spinner e il callback di notifica viene chiamato dalonAnimationEnd metodo di un AnimationListener interno per quello spinner che è impostato fuori servizio in questo modo.

È vero che ci è voluto il nostro tester con le dita più veloci per provocare questa situazione, ma può succedere di tanto in tanto anche in scenari realistici.

Una soluzione per risolvere questo problema è sovrascrivere il onInterceptTouchEventmetodo SwipeRefreshLayoutcome segue:

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

Usa MySwipeRefreshLayoutnel tuo Layout - File e cambia il codice nella soluzione di mhasan in

...

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

...

1
Ho anche avuto lo stesso problema del tuo. La soluzione di nhasan appena modificata con questo pastebin.com/XmfNsDKQ Nota il layout di aggiornamento a scorrimento non crea problemi durante l'aggiornamento, quindi è per questo che il controllo.
Amit Jayant

0

Potrebbe esserci un problema con @huu duy answer quando il ViewPager viene posizionato in un contenitore scorrevole verticalmente che, a sua volta, viene posizionato nel SwiprRefreshLayout Se il contenitore scorrevole del contenuto non è completamente a scorrimento, potrebbe non essere possibile attivare lo scorrimento per aggiornare nello stesso gesto di scorrimento verso l'alto. Infatti, quando inizi a scorrere il contenitore interno e muovi il dito in orizzontale più di mTouchSlop involontariamente (che è 8dp per impostazione predefinita), il CustomSwipeToRefresh proposto rifiuta questo gesto. Quindi un utente deve provare ancora una volta per avviare l'aggiornamento. Questo può sembrare strano per l'utente. Ho estratto il codice sorgente dell'originale SwipeRefreshLayout dalla libreria di supporto al mio progetto e ho riscritto 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;
}

Guarda il mio progetto di esempio su Github .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.