J'ai implémenté le nouveau composant SwipeRefreshLayout
dans mon application et il fonctionne bien avec toutes les vues verticales, comme ListView
, GridView
et ScrollView
.
Il se comporte très mal avec les vues horizontales, comme HorizontalScrollView
. Lorsque vous faites défiler vers la droite ou vers la gauche, la vue SwipeRefreshLayout
met en cache le toucher, empêche le HorizontalScrollView
de le recevoir et commence à faire défiler verticalement pour effectuer le rafraîchissement.
J'ai essayé de résoudre ce problème car j'avais déjà résolu des problèmes avec vertical ScrollView
avec ViewPager
à l'intérieur, en utilisant requestDisallowInterceptTouchEvent
mais cela n'a pas fonctionné. J'ai également remarqué que cette méthode est surchargée dans la classe SwipeRefreshLayout
d'origine sans retourner le super. Le développeur de Google a laissé un commentaire à la place "//Nope.
":)
Comme le composant SwipeRefreshLayout
est relativement nouveau, je n'ai pas trouvé de solution qui résout le problème de défilement horizontal tout en permettant au balayage de rafraîchir la vue pour suivre et gérer le défilement vertical, j'ai donc pensé partager ma solution avec espoir cela épargnera à quelqu'un une heure ou deux.
Je l'ai résolu en étendant SwipeRefreshLayout
et en remplaçant son onInterceptTouchEvent
. À l'intérieur, je calcule si la distance X que l'utilisateur a parcourue est plus grande que la pente tactile. Si c'est le cas, cela signifie que l'utilisateur glisse horizontalement, je renvoie donc false
qui permet à l'enfant de visualiser (le HorizontalScrollView
dans ce cas) pour obtenir l'événement tactile.
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);
}
}
Si vous ne mémorisez pas le fait que vous avez déjà refusé l'événement ACTION_MOVE, vous finirez par le prendre plus tard si l'utilisateur revient près de votre mPrevX initial.
Ajoutez simplement un booléen pour le mémoriser.
public class CustomSwipeToRefresh extends SwipeRefreshLayout {
private int mTouchSlop;
private float mPrevX;
// Indicate if we've already declined the move event
private boolean mDeclined;
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();
mDeclined = false; // New action
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (mDeclined || xDiff > mTouchSlop) {
mDeclined = true; // Memorize
return false;
}
}
return super.onInterceptTouchEvent(event);
}
}
La solution proposée par Lior Iluz avec le remplacement de onInterceptTouchEvent () a un problème sérieux. Si le conteneur défilant du contenu n'est pas entièrement déroulé, il peut ne pas être possible d'activer le balayage vers l'actualisation dans le même geste de défilement vers le haut. En effet, lorsque vous commencez à faire défiler le conteneur interne et déplacez le doigt horizontalement plus que mTouchSlop involontairement (qui est de 8dp par défaut), le CustomSwipeToRefresh proposé décline ce geste. Un utilisateur doit donc essayer à nouveau de commencer à se rafraîchir. Cela peut sembler étrange pour l'utilisateur.
J'ai extrait le code source du SwipeRefreshLayout d'origine de la bibliothèque de support vers mon projet et réécrit le onInterceptTouchEvent (). Le nouveau nom de classe est TouchSafeSwipeRefreshLayout
private boolean mPendingActionDown;
private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
@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 .
Si vous utilisez Tim Roes EnhancedListView
Voir ceci problèmes . J'ai été très utile pour moi car ils ajoutent une fonction qui détecte le début et la fin du balayage.
Lorsque le balayage commence, je désactive le SwipeRefreshLayout et lorsque le balayage se termine, j'active le swipeRefreshLayout.