SwipeRefreshLayout + ViewPager, ¿limitar el desplazamiento horizontal solamente?

94

He implementado SwipeRefreshLayouty ViewPageren mi aplicación, pero hay un gran problema: cada vez que voy a deslizar el dedo hacia la izquierda / derecha para cambiar entre páginas, el desplazamiento es demasiado sensible. Un pequeño deslizamiento hacia abajo también activará la SwipeRefreshLayoutactualización.

Quiero establecer un límite para cuando comience el deslizamiento horizontal, luego forzar horizontalmente solo hasta que termine el deslizamiento. En otras palabras, quiero cancelar el deslizamiento vertical cuando el dedo se mueve horizontalmente.

Este problema solo ocurre en ViewPager, si deslizo hacia abajo y SwipeRefreshLayoutse activa la función de actualización (se muestra la barra) y luego muevo mi dedo horizontalmente, todavía solo permite deslizamientos verticales.

Intenté extender la ViewPagerclase pero no funciona en absoluto:

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

}

Diseño 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>

Cualquier ayuda será apreciada, gracias

user3896501
fuente
¿Funciona el mismo escenario si uno de sus fragmentos dentro del visor tiene un SwipeRefreshLayout?
Zapnologica

Respuestas:

160

No estoy seguro de si todavía tiene este problema, pero la aplicación de E / S de Google iosched resuelve este problema de la siguiente manera:

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

Yo lo he usado igual y funciona bastante bien.

EDITAR: Use addOnPageChangeListener () en lugar de setOnPageChangeListener ().

nhasan
fuente
3
Esta es la mejor respuesta porque tiene en cuenta el estado de ViewPager. No evita un arrastre hacia abajo que se origina en ViewPager, que claramente demuestra una intención de actualizar.
Andrew Gallasch
4
La mejor respuesta, sin embargo, sería bueno publicar el código para enableDisableSwipeRefresh (sí, es obvio por el nombre de la función ... pero para estar seguro de que tuve que buscarlo en Google ...)
Greg Ennis
5
Funciona perfectamente, pero setOnPageChangeListener está depreciado ahora. utilice addOnPageChangeListener en su lugar.
Yon Kornilov
@nhasan A partir de una actualización reciente, esta respuesta ya no funciona. Establecer el estado habilitado de swiperefresh en falso elimina swiperefresh por completo, mientras que antes, si el estado de desplazamiento del visor cambiaba mientras se actualizaba swiperefresh, no eliminaría el diseño, pero lo deshabilitaría mientras lo mantenía en el mismo estado de actualización en el que estaba antes.
Michael Tedla
2
Enlace al código fuente de 'enableDisableSwipeRefresh' en la aplicación de google i / o: android.googlesource.com/platform/external/iosched/+/HEAD/…
jpardogo
37

Resuelto de manera muy simple sin extender nada

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

trabajar como un encanto

user3896501
fuente
De acuerdo, pero tenga en cuenta que tendrá el mismo problema para cualquier desplazamiento Viewque pueda tener dentro del ViewPager, ya que SwipeRefreshLayoutpermite incluso el desplazamiento vertical solo para su hijo de nivel superior (y en API inferiores a ICS solo si resulta ser un ListView) .
corsair992
@ corsair992less Gracias por sus consejos
user3896501
@ corsair992 frente al problema. ¡Tengo ViewPagerdentro SwipeRefrestLayouty ViewPager tiene Listview! SwipeRefreshLayoutdéjame desplazarme hacia abajo pero al desplazarme hacia arriba se activa el progreso de actualización. ¿Cualquier sugerencia?
Muhammad Babar
1
¿Puedes desarrollar un poco más sobre lo que es mLayout?
desgraci
2
viewPager.setOnTouchListener {_, event -> swipeRefreshLayout.isEnabled = event.action == MotionEvent.ACTION_UP false}
Axrorxo'ja Yodgorov
22

He conocido tu problema. Personalizar SwipeRefreshLayout resolvería el 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);
}

Ver la referencia: enlace

huu duy
fuente
Esta es la mejor solución para incluir la pendiente en la detección.
Nafsaka
Gran solución +1
Tram Nguyen
12

Basé esto en una respuesta anterior, pero descubrí que esto funciona un poco mejor. El movimiento comienza con un evento ACTION_MOVE y termina en ACTION_UP o ACTION_CANCEL en mi experiencia.

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
fuente
Thnxx para la solución
Hitesh Kushwah
9

Por alguna razón que solo ellos conocen, el equipo de desarrollo de la biblioteca de soporte consideró oportuno interceptar enérgicamente todos los eventos de movimiento de arrastre vertical del SwipeRefreshLayoutdiseño secundario, incluso cuando un niño solicita específicamente la propiedad del evento. Lo único que verifican es que el estado de desplazamiento vertical de su hijo principal esté en cero (en el caso de que su hijo sea desplazable verticalmente). El requestDisallowInterceptTouchEvent()método se ha anulado con un cuerpo vacío y el comentario (no tan) esclarecedor "No".

La forma más fácil de resolver este problema sería simplemente copiar la clase de la biblioteca de soporte en su proyecto y eliminar la anulación del método. ViewGroupLa implementación de utiliza el estado interno para el manejo onInterceptTouchEvent(), por lo que no puede simplemente anular el método nuevamente y duplicarlo. Si realmente desea anular la implementación de la biblioteca de soporte, entonces tendrá que configurar una marca personalizada en las llamadas requestDisallowInterceptTouchEvent()y anular onInterceptTouchEvent()y onTouchEvent()(o posiblemente piratear canChildScrollUp()) el comportamiento basado en eso.

corsair992
fuente
Hombre, eso es duro. Realmente desearía que no hubieran hecho eso. Tengo una lista que quiero habilitar para que se actualice y la capacidad de deslizar los elementos de la fila. La forma en que han creado SwipeRefreshLayout lo hace casi imposible sin algunas soluciones locas.
Jessie A. Morris
3

Encontré una solución para ViewPager2. Utilizo la reflexión para reducir la sensibilidad al arrastre de esta manera:

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

Funciona de maravilla para mí.

Alex Shevelev
fuente
2

Hay un problema con la solución de nhasan:

Si el deslizamiento horizontal que desencadena la setEnabled(false)llamada SwipeRefreshLayouten el OnPageChangeListenerocurre cuando el SwipeRefreshLayoutya ha reconocido un Pull-to-Reload pero aún no ha llamado a la devolución de llamada de notificación, la animación desaparece, pero el estado interno de SwipeRefreshLayoutpermanece en "refrescando" para siempre, ya que no Se llaman devoluciones de llamada de notificación que podrían restablecer el estado. Desde la perspectiva del usuario, esto significa que Pull-to-Reload ya no funciona porque no se reconocen todos los gestos de extracción.

El problema aquí es que la disable(false)llamada elimina la animación de la ruleta y la devolución de llamada de notificación se llama desde el onAnimationEndmétodo de un AnimationListener interno para esa ruleta que está desordenada de esa manera.

Es cierto que nuestro probador, con los dedos más rápidos, provocó esta situación, pero también puede suceder de vez en cuando en escenarios realistas.

Una solución para solucionar esto es anular el onInterceptTouchEventmétodo de la SwipeRefreshLayoutsiguiente manera:

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

Use el MySwipeRefreshLayouten su Diseño - Archivo y cambie el código en la solución de mhasan a

...

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

...
Nantoka
fuente
1
También tuve el mismo problema que el tuyo. Acabo de modificar la solución de nhasan con este pastebin.com/XmfNsDKQ Note que el diseño de actualización por deslizamiento no crea problemas cuando se actualiza, por eso la verificación.
Amit Jayant
0

Podría haber un problema con @huu duy answer cuando ViewPager se coloca en un contenedor de desplazamiento vertical que, a su vez, se coloca en SwiprRefreshLayout. Si el contenedor de contenido desplazable no se desplaza completamente hacia arriba, es posible que no sea posible activar deslizar para actualizar en el mismo gesto de desplazamiento hacia arriba. De hecho, cuando comienza a desplazarse por el contenedor interno y mueve el dedo horizontalmente más que mTouchSlop involuntariamente (que es 8dp por defecto), el CustomSwipeToRefresh propuesto rechaza este gesto. Por lo tanto, un usuario debe intentarlo una vez más para comenzar a actualizar. Esto puede parecer extraño para el usuario. Extraje el código fuente del SwipeRefreshLayout original de la biblioteca de soporte a mi proyecto y reescribí el 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;
}

Vea mi proyecto de ejemplo en Github .

Stanislav Perchenko
fuente