web-dev-qa-db-fra.com

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

J'ai implémenté SwipeRefreshLayout et ViewPager dans mon application, mais il y a un gros problème: chaque fois que je vais balayer l'écran vers la gauche ou la droite pour changer de page, le défilement est trop sensible. Un petit coup vers le bas déclenchera également le rafraîchissement de SwipeRefreshLayout.

Je souhaite définir une limite au début du balayage horizontal, puis forcer le mouvement horizontal 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 sur ViewPager, si je glisse vers le bas et que la fonction d'actualisation SwipeRefreshLayout est déclenchée (la barre est affichée), puis que je déplace mon doigt horizontalement, cela n'autorise toujours que les glissements verticaux.

J'ai essayé d'étendre la classe ViewPager 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;
    }

}

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>

toute aide serait appréciée Merci

84
user3896501

Résolue très simplement sans prolonger quoi que ce soit

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

travailler comme un charme

34
user3896501

Je ne sais pas si vous avez toujours ce problème, mais l'application Google I/O iosched résout ce problème de la manière suivante:

    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 ().

141
nhasan

J'ai rencontré ton 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 réf: lien

22
huu duy

J'ai basé ceci sur une réponse précédente, mais j'ai trouvé que cela fonctionnait un peu mieux. La requête commence par un événement ACTION_MOVE et se termine par ACTION_UP ou ACTION_CANCEL, selon 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;
    }
});
10
Sean Abraham

Pour une raison mieux connue de tous, l'équipe de développement de la bibliothèque de support technique a jugé bon d'interrompre avec force tous les événements de mouvement de traînée verticale de l'enfant de SwipeRefreshLayout mise en page, 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éfilement vertical). La méthode requestDisallowInterceptTouchEvent() a été remplacée par un corps vide et le commentaire éclairant "Nope".

Le moyen le plus simple de résoudre ce problème consiste à simplement copier la classe de la bibliothèque de support dans votre projet et à supprimer le remplacement de méthode. L'implémentation de ViewGroup utilise l'état interne pour gérer onInterceptTouchEvent(), vous ne pouvez donc pas simplement redéfinir la méthode et la dupliquer. Si vous voulez vraiment remplacer la mise en œuvre de la bibliothèque de support , vous devrez configurer un indicateur personnalisé lors de l'appel de requestDisallowInterceptTouchEvent(), et remplacer onInterceptTouchEvent() et onTouchEvent() (ou éventuellement bidouiller canChildScrollUp()) un comportement basé sur ce comportement.

9
corsair992

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.

1
Alex Shevelev

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

Si le balayage horizontal qui déclenche l'appel setEnabled(false) sur le SwipeRefreshLayout du OnPageChangeListener se produit lorsque le SwipeRefreshLayout a déjà reconnu un processus Pull-to-Reload mais n’a pas encore appelé le rappel de notification, l’animation disparaît mais l’état interne de la SwipeRefreshLayout reste toujours "en cours de rafraîchissement" car aucun rappel de notification n’est appelé qui pourrait réinitialiser l’état. Du point de vue de l'utilisateur, cela signifie que la procédure d'extraction au rechargement ne fonctionne plus car tous les gestes d'extraction ne sont pas reconnus.

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

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

Une solution pour y remédier consiste à remplacer la méthode onInterceptTouchEvent dans SwipeRefreshLayout comme 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 le MySwipeRefreshLayout dans votre Layout - File et changez le code dans la solution de mhasan en

...

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

...
1
Nantoka

@Huu duy answer pourrait poser un problème lorsque ViewPager est placé dans un conteneur à défilement vertical qui, à son tour, est placé dans SwiprRefreshLayout Si le conteneur à contenu défilable n’est pas entièrement déroulé, il est alors impossible Activer le glissement pour actualiser dans le même geste de défilement. En effet, lorsque vous commencez à faire défiler le conteneur interne et que vous déplacez le doigt horizontalement de plus de mTouchSlop par inadvertance (ce qui correspond à 8dp par défaut), le CustomSwipeToRefresh proposé décline ce geste. Donc, un utilisateur doit essayer une fois de plus pour commencer l'actualisation. Cela peut paraître étrange pour l'utilisateur. J'ai extrait le code source de SwipeRefreshLayout d'origine de la bibliothèque de support de mon projet et ai réécrit la méthode 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 .

0