Ajuste de desplazamiento horizontal de RecyclerView en el centro

89

Estoy tratando de hacer una vista similar a un carrusel aquí usando RecyclerView, quiero que el elemento se ajuste en el medio de la pantalla cuando se desplaza, un elemento a la vez. He intentado usarrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

pero la vista todavía se desplaza sin problemas, también intenté implementar mi propia lógica usando el oyente de desplazamiento así:

recyclerView.setOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    Log.v("Offset ", recyclerView.getWidth() + "");
                    if (newState == 0) {
                        try {
                               recyclerView.smoothScrollToPosition(layoutManager.findLastVisibleItemPosition());
                                recyclerView.scrollBy(20,0);
                            if (layoutManager.findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - 1) {
                                Beam refresh = new Beam();
                                refresh.execute(createUrl());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

El deslizamiento de derecha a izquierda funciona bien ahora, pero no al revés, ¿qué me estoy perdiendo aquí?

Ahmed Awad
fuente

Respuestas:

161

Con LinearSnapHelper, esto ahora es muy fácil.

Todo lo que necesitas hacer es esto:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);

Actualizar

Disponible desde 25.1.0, PagerSnapHelperpuede ayudar a lograr el ViewPagermismo efecto. Úselo como lo haría con el LinearSnapHelper.

Solución alternativa antigua:

Si desea que se comporte de manera similar al ViewPager, intente esto en su lugar:

LinearSnapHelper snapHelper = new LinearSnapHelper() {
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        View centerView = findSnapView(layoutManager);
        if (centerView == null) 
            return RecyclerView.NO_POSITION;

        int position = layoutManager.getPosition(centerView);
        int targetPosition = -1;
        if (layoutManager.canScrollHorizontally()) {
            if (velocityX < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        if (layoutManager.canScrollVertically()) {
            if (velocityY < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        final int firstItem = 0;
        final int lastItem = layoutManager.getItemCount() - 1;
        targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
        return targetPosition;
    }
};
snapHelper.attachToRecyclerView(recyclerView);

La implementación anterior simplemente devuelve la posición junto al elemento actual (centrado) en función de la dirección de la velocidad, independientemente de la magnitud.

La primera es una solución de primera parte incluida en la biblioteca de soporte versión 24.2.0. Lo que significa que debe agregar esto al módulo de su aplicación build.gradleo actualizarlo.

compile "com.android.support:recyclerview-v7:24.2.0"
Razzle Dazzle
fuente
Esto no funcionó para mí (pero la solución de @Albert Vila Calvo sí). ¿Lo has implementado? ¿Se supone que debemos anular métodos de él? Creo que la clase debería hacer todo el trabajo fuera de la caja
galaxigirl
Sí, de ahí la respuesta. Sin embargo, no he probado a fondo. Para ser claros, hice esto con una horizontal LinearLayoutManagerdonde todas las vistas eran de tamaño regular. No se necesita nada más que el fragmento de arriba.
razzledazzle
@galaxigirl disculpas, he entendido lo que querías decir y he hecho una edición reconociendo eso, por favor comenta Esta es una solución alternativa con LinearSnapHelper.
razzledazzle
Esto funcionó bien para mí. @galaxigirl anular este método ayuda a que el asistente de ajuste lineal se ajuste al elemento más cercano cuando el desplazamiento se estabiliza, no exactamente al elemento siguiente / anterior. Muchas gracias por esto, razzledazzle
AA_PV
LinearSnapHelper fue la solución inicial que funcionó para mí, pero parece que PagerSnapHelper le dio al deslizamiento una mejor animación.
LeonardoSibela
76

Actualización de Google I / O 2019

¡ViewPager2 ya está aquí!

Google acaba de anunciar en la charla 'Novedades de Android' (también conocido como 'La nota clave de Android') que están trabajando en un nuevo ViewPager basado en RecyclerView.

De las diapositivas:

Como ViewPager, pero mejor

  • Fácil migración desde ViewPager
  • Basado en RecyclerView
  • Soporte de modo de derecha a izquierda
  • Permite la paginación vertical
  • Notificaciones de cambio de conjunto de datos mejoradas

Puede consultar la última versión aquí y las notas de la versión aquí . También hay una muestra oficial .

Opinión personal: Creo que esta es una adición realmente necesaria. Recientemente tuve muchos problemas con la PagerSnapHelper oscilación de izquierda a derecha indefinidamente ; vea el boleto que abrí.


Nueva respuesta (2016)

Ahora puede usar SnapHelper .

Si desea un comportamiento de ajuste alineado al centro similar a ViewPager , use PagerSnapHelper :

SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

También hay un LinearSnapHelper . Lo he probado y si arrojas con energía, se desplaza 2 elementos con 1 lanzamiento. Personalmente, no me gustó, pero solo decide tú mismo: intentarlo solo lleva unos segundos.


Respuesta original (2016)

Después de muchas horas de probar 3 soluciones diferentes que se encuentran aquí en SO, finalmente construí una solución que imita muy de cerca el comportamiento encontrado en un ViewPager.

La solución se basa en la solución @eDizzle , que creo que he mejorado lo suficiente como para decir que funciona casi como un ViewPager.

Importante: el RecyclerViewancho de mis artículos es exactamente el mismo que el de la pantalla. No lo he probado con otros tamaños. También lo uso con una horizontal LinearLayoutManager. Creo que necesitará adaptar el código si desea un desplazamiento vertical.

Aquí tienes el código:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://stackoverflow.com/a/29171652/4034572

    public SnappyRecyclerView(Context context) {
        super(context);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

        int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

        // views on the screen
        int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
        View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
        int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
        View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

        // distance we need to scroll
        int leftMargin = (screenWidth - lastView.getWidth()) / 2;
        int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
        int leftEdge = lastView.getLeft();
        int rightEdge = firstView.getRight();
        int scrollDistanceLeft = leftEdge - leftMargin;
        int scrollDistanceRight = rightMargin - rightEdge;

        if (Math.abs(velocityX) < 1000) {
            // The fling is slow -> stay at the current page if we are less than half through,
            // or go to the next page if more than half through

            if (leftEdge > screenWidth / 2) {
                // go to next page
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                // go to next page
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                // stay at current page
                if (velocityX > 0) {
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else {
                    smoothScrollBy(scrollDistanceLeft, 0);
                }
            }
            return true;

        } else {
            // The fling is fast -> go to next page

            if (velocityX > 0) {
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                smoothScrollBy(-scrollDistanceRight, 0);
            }
            return true;

        }

    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);

        // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
        // This code fixes this. This code is not strictly necessary but it improves the behaviour.

        if (state == SCROLL_STATE_IDLE) {
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

            int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

            // views on the screen
            int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
            int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
            View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

            // distance we need to scroll
            int leftMargin = (screenWidth - lastView.getWidth()) / 2;
            int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
            int leftEdge = lastView.getLeft();
            int rightEdge = firstView.getRight();
            int scrollDistanceLeft = leftEdge - leftMargin;
            int scrollDistanceRight = rightMargin - rightEdge;

            if (leftEdge > screenWidth / 2) {
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                smoothScrollBy(scrollDistanceLeft, 0);
            }
        }
    }

}

¡Disfrutar!

Albert Vila Calvo
fuente
Agradable coger el onScrollStateChanged (); de lo contrario, existe un pequeño error si deslizamos lentamente el RecyclerView. ¡Gracias!
Mauricio Sartori
Para admitir márgenes (android: layout_marginStart = "16dp") lo intenté con int screenwidth = getWidth (); Y parece funcionar.
eigil
AWESOOOMMEEEEEEEEE
raditya gumay
1
@galaxigirl no, todavía no he probado LinearSnapHelper. Acabo de actualizar la respuesta para que la gente conozca la solución oficial. Mi solución funciona sin ningún problema en mi aplicación.
Albert Vila Calvo
1
@AlbertVilaCalvo Probé LinearSnapHelper. LinearSnapHelper funciona con un Scroller. Se desplaza y se rompe según la gravedad y la velocidad de lanzamiento. A mayor velocidad, más páginas se desplazan. No lo recomendaría para un comportamiento similar a ViewPager.
mipreamble
46

Si el objetivo es RecyclerViewimitar el comportamiento, ViewPagerhay un enfoque bastante sencillo.

RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);

LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
SnapHelper snapHelper = new PagerSnapHelper();
recyclerView.setLayoutManager(layoutManager);
snapHelper.attachToRecyclerView(mRecyclerView);

Al usarlo PagerSnapHelper, puede obtener el comportamiento comoViewPager

Boonya Kitpitak
fuente
4
Estoy usando en LinearSnapHelperlugar dePagerSnapHelper y funciona para mí
fanjavaid
1
Sí, no hay efecto instantáneo enLinearSnapHelper
Boonya Kitpitak
1
esto pega el elemento en la vista habilita scrolable
Sam
@BoonyaKitpitak, lo intenté LinearSnapHelpery se ajusta a un elemento del medio. PagerSnapHelperimpide desplazarse fácilmente (al menos, una lista de imágenes).
CoolMind
@BoonyaKitpitak, ¿puede decirme cómo hacer esto, un elemento en el centro completamente visible y los elementos laterales son la mitad de visibles?
Rucha Bhatt Joshi
30

Debe usar findFirstVisibleItemPosition para ir en la dirección opuesta. Y para detectar en qué dirección estaba el deslizamiento, necesitará obtener la velocidad de lanzamiento o el cambio en x. Abordé este problema desde un ángulo ligeramente diferente al tuyo.

Cree una nueva clase que amplíe la clase RecyclerView y luego anule el método de lanzamiento de RecyclerView así:

@Override
public boolean fling(int velocityX, int velocityY) {
    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

//these four variables identify the views you see on screen.
    int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
    int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
    View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
    View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);

//these variables get the distance you need to scroll in order to center your views.
//my views have variable sizes, so I need to calculate side margins separately.     
//note the subtle difference in how right and left margins are calculated, as well as
//the resulting scroll distances.
    int leftMargin = (screenWidth - lastView.getWidth()) / 2;
    int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
    int leftEdge = lastView.getLeft();
    int rightEdge = firstView.getRight();
    int scrollDistanceLeft = leftEdge - leftMargin;
    int scrollDistanceRight = rightMargin - rightEdge;

//if(user swipes to the left) 
    if(velocityX > 0) smoothScrollBy(scrollDistanceLeft, 0);
    else smoothScrollBy(-scrollDistanceRight, 0);

    return true;
}
eDizzle
fuente
¿Dónde se define el ancho de pantalla?
ltsai
@Itsai: ¡Ups! Buena pregunta. Es una variable que configuré en otra parte de la clase. Es el ancho de la pantalla del dispositivo en píxeles, no en píxeles de densidad, ya que el método smoothScrollBy requiere la cantidad de píxeles por los que desea que se desplace.
eDizzle
3
@ltsai Puede cambiar a getWidth () (recyclerview.getWidth ()) en lugar de screenWidth. screenWidth no funciona correctamente cuando RecyclerView es más pequeño que el ancho de la pantalla.
VAdaihiep
Intenté extender RecyclerView pero obtengo "Error al inflar la clase personalizada RecyclerView". ¿Puede usted ayudar? Gracias
@bEtTyBarnes: es posible que desee buscar eso en otro feed de preguntas. Está bastante fuera de alcance con la pregunta inicial aquí.
eDizzle
6

Sólo tiene que añadir paddingy margina recyclerViewyrecyclerView item :

reciclador

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentLayout"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_marginLeft="8dp" <!-- here -->
    android:layout_marginRight="8dp" <!-- here  -->
    android:layout_width="match_parent"
    android:layout_height="200dp">

   <!-- child views -->

</RelativeLayout>

recicladorView:

<androidx.recyclerview.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp" <!-- here -->
    android:paddingRight="8dp" <!-- here -->
    android:clipToPadding="false" <!-- important!-->
    android:scrollbars="none" />

y establecer PagerSnapHelper:

int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
parentLayout.getLayoutParams().width = displayWidth - Utils.dpToPx(16) * 4;
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

dp a px:

public static int dpToPx(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}

resultado:

ingrese la descripción de la imagen aquí

Farzad
fuente
¿Hay alguna forma de lograr un relleno variable entre elementos? como si la primera y la última carta no necesitaran estar centradas para mostrar más de las cartas siguientes / previas en comparación con las cartas intermedias que se están centrando?
RamPrasadBismil
2

Mi solución:

/**
 * Horizontal linear layout manager whose smoothScrollToPosition() centers
 * on the target item
 */
class ItemLayoutManager extends LinearLayoutManager {

    private int centeredItemOffset;

    public ItemLayoutManager(Context context) {
        super(context, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    public void setCenteredItemOffset(int centeredItemOffset) {
        this.centeredItemOffset = centeredItemOffset;
    }

    /**
     * ********** Inner Classes **********
     */

    private class Scroller extends LinearSmoothScroller {

        public Scroller(Context context) {
            super(context);
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return ItemLayoutManager.this.computeScrollVectorForPosition(targetPosition);
        }

        @Override
        public int calculateDxToMakeVisible(View view, int snapPreference) {
            return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centeredItemOffset;
        }
    }
}

Paso este administrador de diseño a RecycledView y configuro el desplazamiento requerido para centrar los elementos. Todos mis artículos tienen el mismo ancho, por lo que el desplazamiento constante está bien

anagaf
fuente
0

PagerSnapHelperno funciona con GridLayoutManagerspanCount> 1, por lo que mi solución en esta circunstancia es:

class GridPagerSnapHelper : PagerSnapHelper() {
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
        val forwardDirection = if (layoutManager?.canScrollHorizontally() == true) {
            velocityX > 0
        } else {
            velocityY > 0
        }
        val centerPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
        return centerPosition +
            if (forwardDirection) (layoutManager as GridLayoutManager).spanCount - 1 else 0
    }
}
Wenqing MA
fuente