SwipeRefreshLayout + ViewPager, nur horizontalen Bildlauf einschränken?


94

Ich habe SwipeRefreshLayoutund ViewPagerin meiner App implementiert, aber es gibt ein großes Problem: Wenn ich nach links / rechts wische, um zwischen den Seiten zu wechseln, ist das Scrollen zu empfindlich. Ein kleines Wischen nach unten löst auch die SwipeRefreshLayoutAktualisierung aus.

Ich möchte ein Limit festlegen, wann das horizontale Wischen beginnt, und dann das horizontale Wischen nur erzwingen, bis das Wischen beendet ist. Mit anderen Worten, ich möchte das vertikale Wischen abbrechen, wenn sich der Finger horizontal bewegt.

Dieses Problem tritt nur auf ViewPager, wenn ich nach unten wische und die SwipeRefreshLayoutAktualisierungsfunktion ausgelöst wird (der Balken wird angezeigt) und dann meinen Finger horizontal bewege. Es sind immer noch nur vertikale Wischbewegungen zulässig.

Ich habe versucht, die ViewPagerKlasse zu erweitern , aber es funktioniert überhaupt nicht:

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>

Jede Hilfe wäre dankbar, danke


Funktioniert das gleiche Szenario, wenn eines Ihrer Fragmente im Viewpager eine hat SwipeRefreshLayout?
Zapnologica

Antworten:


160

Ich bin nicht sicher, ob Sie dieses Problem noch haben, aber Google I / O App iosched löst dieses Problem folgendermaßen:

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

Ich habe das gleiche benutzt und funktioniert ganz gut.

BEARBEITEN: Verwenden Sie addOnPageChangeListener () anstelle von setOnPageChangeListener ().


3
Dies ist die beste Antwort, da sie den Status des ViewPagers berücksichtigt. Es verhindert nicht ein Herunterziehen nach unten, das vom ViewPager ausgeht, was eindeutig die Absicht einer Aktualisierung zeigt.
Andrew Gallasch

4
Beste Antwort, aber es könnte schön sein, den Code zu aktivieren, um DisableSwipeRefresh zu aktivieren (ja, es ist aus dem Funktionsnamen ersichtlich ... aber um sicher zu sein, dass ich ihn googeln musste ...)
Greg Ennis

5
Funktioniert einwandfrei, aber setOnPageChangeListener wird jetzt abgeschrieben. Verwenden Sie stattdessen addOnPageChangeListener.
Yon

@nhasan Seit einem aktuellen Update funktioniert diese Antwort nicht mehr. Wenn Sie den aktivierten Status von swiperefresh auf false setzen, wird das swiperefresh vollständig entfernt. Wenn sich zuvor der Bildlaufstatus des Viewpagers während der Aktualisierung von swiperefresh geändert hat, wird das Layout nicht entfernt, sondern deaktiviert, während der Aktualisierungsstatus beibehalten wird.
Michael Tedla

2
Link zum Quellcode für 'enableDisableSwipeRefresh' in der Google I / O-App: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo

37

Sehr einfach gelöst, ohne etwas zu erweitern

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

arbeite wie ein Zauber


Ok, aber denken Sie daran, dass Sie das gleiche Problem für alle Bildlauffunktionen haben View, die Sie möglicherweise im Inneren haben ViewPager, da SwipeRefreshLayoutsogar vertikales Bildlauf nur für das untergeordnete Element der obersten Ebene möglich ist (und in APIs, die niedriger als ICS sind, nur, wenn es sich zufällig um a handelt ListView). .
Corsair992

@ corsair992less Vielen Dank für Ihre Tipps
user3896501

@ corsair992 vor dem Problem. Ich habe ViewPagerdrinnen SwipeRefrestLayoutund der ViewPager hat Listview! SwipeRefreshLayoutLassen Sie mich nach unten scrollen, aber beim Scrollen nach oben wird der Aktualisierungsfortschritt ausgelöst. Irgendein Vorschlag?
Muhammad Babar

1
Können Sie etwas mehr darüber entwickeln, was mLayout ist?
Desgraci

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

22

Ich habe dein Problem getroffen. Passen Sie das SwipeRefreshLayout an, um das Problem zu lösen.

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

Siehe den Link ref:


Dies ist die beste Lösung, um die Steigung in die Erkennung einzubeziehen.
Nafsaka

Tolle Lösung +1
Tram Nguyen

11

Ich habe dies auf eine frühere Antwort gestützt, fand aber, dass dies etwas besser funktioniert. Die Bewegung beginnt mit einem ACTION_MOVE-Ereignis und endet meiner Erfahrung nach entweder mit ACTION_UP oder ACTION_CANCEL.

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

Danke für die Lösung
Hitesh Kushwah

9

Aus irgendeinem Grund , um sie am besten bekannt nur, die Support - Bibliothek Entwickler - Team sah fit zu gewaltsam abfangen alle vertikalen Schleppbewegung Ereignisse von SwipeRefreshLayout‚s Kind Layout, auch wenn ein Kind das Eigentum an der Veranstaltung ausdrücklich anfordert. Das einzige, worauf sie prüfen, ist, dass der vertikale Bildlaufstatus des Hauptkindes Null ist (für den Fall, dass das Kind vertikal scrollbar ist). DasrequestDisallowInterceptTouchEvent() Methode wurde mit einem leeren Körper und dem (nicht so) leuchtenden Kommentar "Nope" überschrieben.

Der einfachste Weg, um dieses Problem zu lösen, besteht darin, die Klasse aus der Support-Bibliothek in Ihr Projekt zu kopieren und die Methodenüberschreibung zu entfernen. ViewGroupDie Implementierung verwendet den internen Status für die Behandlung onInterceptTouchEvent(), sodass Sie die Methode nicht einfach erneut überschreiben und duplizieren können. Wenn Sie die Implementierung der Support-Bibliothek wirklich überschreiben möchten , müssen Sie bei Aufrufen ein benutzerdefiniertes Flag einrichten requestDisallowInterceptTouchEvent()und das darauf basierende Verhalten überschreiben onInterceptTouchEvent()und onTouchEvent()(oder möglicherweise hacken canChildScrollUp()).


Mann, das ist rau. Ich wünschte wirklich, sie hätten das nicht getan. Ich habe eine Liste, die Pull zum Aktualisieren aktivieren soll, und die Möglichkeit, die Zeilenelemente zu wischen. Die Art und Weise, wie sie das SwipeRefreshLayout erstellt haben, macht dies ohne verrückte Umgehungen fast unmöglich.
Jessie A. Morris

3

Ich habe eine Lösung für ViewPager2 gefunden. Ich benutze Reflexion, um die Widerstandsempfindlichkeit wie folgt zu reduzieren:

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

Es funktioniert wie ein Zauber für mich.


2

Es gibt ein Problem mit der Lösung von nhasan:

Wenn der horizontale Wisch, der den setEnabled(false)Anruf auf dem SwipeRefreshLayoutin auslöst, auftritt, OnPageChangeListenerwenn der SwipeRefreshLayoutbereits einen Pull-to-Reload erkannt hat, aber den Benachrichtigungsrückruf noch nicht aufgerufen hat, verschwindet die Animation, aber der interne Status der SwipeRefreshLayoutbleibt für immer auf "Aktualisieren" als Nein Es werden Benachrichtigungsrückrufe aufgerufen, die den Status zurücksetzen könnten. Aus Anwendersicht bedeutet dies, dass Pull-to-Reload nicht mehr funktioniert, da nicht alle Pull-Gesten erkannt werden.

Das Problem hierbei ist, dass der disable(false)Aufruf die Animation des Spinners entfernt und der Benachrichtigungsrückruf von der onAnimationEndMethode eines internen AnimationListener für diesen Spinner aufgerufen wird, der auf diese Weise nicht in der richtigen Reihenfolge eingestellt ist.

Zwar brauchte unser Tester mit den schnellsten Fingern, um diese Situation zu provozieren, aber es kann auch in realistischen Szenarien gelegentlich vorkommen.

Eine Lösung, um dies zu beheben, besteht darin, die onInterceptTouchEventMethode SwipeRefreshLayoutwie folgt zu überschreiben :

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

Verwenden Sie die MySwipeRefreshLayoutin Ihrem Layout - Datei und ändern Sie den Code in der Lösung von mhasan in

...

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

...

1
Ich hatte auch das gleiche Problem wie deins. Gerade die Lösung von nhasan mit diesem pastebin.com/XmfNsDKQ Hinweis geändert. Das Layout für das Aktualisieren von Wischbewegungen verursacht beim Aktualisieren keine Probleme.
Amit Jayant

0

Es kann ein Problem mit der Antwort von @huu duy auftreten, wenn der ViewPager in einem vertikal scrollbaren Container platziert wird, der wiederum im SwiprRefreshLayout platziert wird. Wenn der inhaltsrollbare Container nicht vollständig nach oben gescrollt ist, ist dies möglicherweise nicht möglich Aktivieren Sie das Wischen zum Aktualisieren in derselben Bildlaufgeste. In der Tat lehnt das vorgeschlagene CustomSwipeToRefresh diese Geste ab, wenn Sie mit dem Scrollen des inneren Containers beginnen und den Finger mehr als mTouchSlop unbeabsichtigt horizontal bewegen (dies ist standardmäßig 8 dp). Ein Benutzer muss also erneut versuchen, mit der Aktualisierung zu beginnen. Dies kann für den Benutzer seltsam aussehen. Ich habe den Quellcode des ursprünglichen SwipeRefreshLayout aus der Support-Bibliothek in mein Projekt extrahiert und das onInterceptTouchEvent () neu geschrieben.

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

Siehe mein Beispielprojekt auf Github .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.