SwipeRefreshLayout + ViewPager, limiter le défilement horizontal uniquement?

94

J'ai implémenté SwipeRefreshLayoutet ViewPagerdans mon application mais il y a un gros problème: chaque fois que je vais balayer vers la gauche / droite pour basculer entre les pages, le défilement est trop sensible. Un petit glissement vers le bas déclenchera également l' SwipeRefreshLayoutactualisation.

Je souhaite définir une limite au début du balayage horizontal, puis forcer l'horizontale uniquement jusqu'à ce que le balayage soit terminé. En d'autres termes, je souhaite annuler le balayage vertical lorsque le doigt se déplace horizontalement.

Ce problème ne se produit que ViewPagersi je glisse vers le bas et que la SwipeRefreshLayoutfonction d'actualisation est déclenchée (la barre est affichée) et que je déplace mon doigt horizontalement, cela ne permet toujours que les balayages verticaux.

J'ai essayé d'étendre la ViewPagerclasse mais cela ne fonctionne pas du tout:

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

}

Mise en page 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>

toute aide serait appréciée Merci

user3896501
la source
Le même scénario fonctionne-t-il si l'un de vos fragments à l'intérieur du visualiseur a un SwipeRefreshLayout?
Zapnologica

Réponses:

160

Je ne sais pas si vous rencontrez toujours ce problème, mais l'application i / O Google I / O résout ce problème ainsi:

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

J'ai utilisé le même et fonctionne assez bien.

EDIT: Utilisez addOnPageChangeListener () au lieu de setOnPageChangeListener ().

Nhasan
la source
3
C'est la meilleure réponse car elle tient compte de l'état du ViewPager. Cela n'empêche pas un glissement vers le bas qui provient du ViewPager, ce qui démontre clairement une intention d'actualisation.
Andrew Gallasch
4
Meilleure réponse, mais il serait peut-être bien de poster le code pour enableDisableSwipeRefresh (oui c'est évident à partir du nom de la fonction .. mais pour être sûr que je devais le rechercher sur Google ...)
Greg Ennis
5
Fonctionne parfaitement, mais setOnPageChangeListener est déprécié maintenant. utilisez plutôt addOnPageChangeListener.
Yon Kornilov
@nhasan Depuis une mise à jour récente, cette réponse ne fonctionne plus. La définition de l'état activé de swiperefresh sur false supprime complètement le swiperefresh, alors qu'avant, si l'état de défilement du visualiseur changeait pendant que le swiperefresh est en cours d'actualisation, il ne supprimerait pas la mise en page, mais le désactiverait tout en le conservant dans le même état d'actualisation qu'il était auparavant.
Michael Tedla
2
Lien vers le code source de 'enableDisableSwipeRefresh' dans l'application google i / o: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo
37

Résolu très simplement sans rien étendre

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

travaille comme un charme

user3896501
la source
Ok, mais gardez à l'esprit que vous aurez ce même problème pour tout scrollable Viewque vous pourriez avoir à l'intérieur du ViewPager, car il SwipeRefreshLayoutpermet même le défilement vertical uniquement pour son enfant de niveau supérieur (et dans les API inférieures à ICS uniquement s'il se trouve que c'est un ListView) .
corsair992
@ corsair992less Merci pour vos conseils
user3896501
@ corsair992 face au problème. J'ai à l' ViewPagerintérieur SwipeRefrestLayoutet le ViewPager a Listview! SwipeRefreshLayoutlaissez-moi faire défiler vers le bas, mais en faisant défiler vers le haut, cela déclenche la progression de l'actualisation. Toute suggestion?
Muhammad Babar
1
pouvez-vous développer un peu plus sur ce qu'est mLayout?
desgraci
2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov
22

J'ai rencontré votre problème. Personnaliser le SwipeRefreshLayout résoudrait le problème.

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

Voir la ref: lien

huu duy
la source
C'est la meilleure solution, pour inclure la pente dans la détection.
nafsaka
Excellente solution +1
Tram Nguyen
12

Je me suis basé sur une réponse précédente, mais j'ai trouvé que cela fonctionnait un peu mieux. Le mouvement commence par un événement ACTION_MOVE et se termine par ACTION_UP ou ACTION_CANCEL d'après mon expérience.

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
la source
Thnxx pour la solution
Hitesh Kushwah
9

Pour une raison connue d'eux seuls, l'équipe de développement de la bibliothèque de support a jugé bon d' intercepter de force tous les événements de mouvement de traînée verticale de SwipeRefreshLayoutla disposition enfant de, même lorsqu'un enfant demande spécifiquement la propriété de l'événement. La seule chose qu'ils vérifient est que l'état de défilement vertical de son enfant principal est à zéro (dans le cas où son enfant est défilable verticalement). La requestDisallowInterceptTouchEvent()méthode a été remplacée par un corps vide et le (pas si) commentaire éclairant "Nope".

Le moyen le plus simple de résoudre ce problème serait de simplement copier la classe de la bibliothèque de support dans votre projet et de supprimer le remplacement de méthode. ViewGroupL'implémentation de utilise l'état interne pour la gestion onInterceptTouchEvent(), vous ne pouvez donc pas simplement remplacer à nouveau la méthode et la dupliquer. Si vous voulez vraiment remplacer l'implémentation de la bibliothèque de support, vous devrez configurer un indicateur personnalisé lors des appels à requestDisallowInterceptTouchEvent(), et remplacer onInterceptTouchEvent()et onTouchEvent()(ou éventuellement pirater canChildScrollUp()) le comportement en fonction de cela.

corsaire992
la source
Mec, c'est dur. J'aurais vraiment aimé qu'ils ne l'aient pas fait. J'ai une liste que je souhaite activer Pull pour actualiser et la possibilité de faire glisser les éléments de ligne. La façon dont ils ont créé le SwipeRefreshLayout rend cela presque impossible sans un travail fou.
Jessie A. Morris
3

J'ai trouvé une solution pour ViewPager2. J'utilise la réflexion pour réduire la sensibilité de la traînée comme ceci:

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

Cela fonctionne comme un charme pour moi.

Alex Shevelev
la source
2

Il y a un problème avec la solution de nhasan:

Si le balayage horizontal qui déclenche l' setEnabled(false)appel sur le SwipeRefreshLayoutdans le OnPageChangeListenerse produit alors que le SwipeRefreshLayouta déjà reconnu un Pull-to-Reload mais n'a pas encore appelé le rappel de notification, l'animation disparaît mais l'état interne du SwipeRefreshLayoutreste sur "rafraîchissant" pour toujours car non des rappels de notification sont appelés qui pourraient réinitialiser l'état. Du point de vue de l'utilisateur, cela signifie que Pull-to-Reload ne fonctionne plus car tous les mouvements de traction ne sont pas reconnus.

Le problème ici est que l' disable(false)appel supprime l'animation du spinner et que le rappel de notification est appelé à partir de la onAnimationEndméthode d'un AnimationListener interne pour ce spinner qui est mis dans le désordre de cette façon.

Il a certes fallu notre testeur avec les doigts les plus rapides pour provoquer cette situation, mais cela peut également arriver de temps en temps dans des scénarios réalistes.

Une solution pour résoudre ce problème consiste à remplacer la onInterceptTouchEventméthode SwipeRefreshLayoutcomme suit:

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

Utilisez MySwipeRefreshLayoutdans votre mise en page - Fichier et modifiez le code de la solution de mhasan en

...

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

...
Nantoka
la source
1
J'ai aussi eu le même problème que le vôtre. Je viens de modifier la solution de nhasan avec ce pastebin.com/XmfNsDKQ Remarque La mise en page d'actualisation par balayage ne crée pas de problème lors de son rafraîchissement, c'est pourquoi la vérification.
Amit Jayant
0

Il peut y avoir un problème avec la réponse @huu duy lorsque le ViewPager est placé dans un conteneur à défilement vertical qui, à son tour, est placé dans le SwiprRefreshLayout Si le conteneur à défilement du contenu n'est pas entièrement défilé vers le haut, il peut être impossible de activez le balayage pour actualiser dans le même geste de défilement vers le haut. En effet, lorsque vous commencez à faire défiler le conteneur intérieur et à déplacer le doigt horizontalement plus que mTouchSlop involontairement (qui est de 8dp par défaut), le CustomSwipeToRefresh proposé refuse ce geste. Un utilisateur doit donc essayer une fois de plus pour commencer à actualiser. Cela peut paraître étrange pour l'utilisateur. J'ai extrait le code source du SwipeRefreshLayout original de la bibliothèque de support vers mon projet et réécrit le 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;
}

Voir mon exemple de projet sur Github .

Stanislav Perchenko
la source