RecyclerView: ¿cómo deslizar suavemente hasta la parte superior del elemento en una determinada posición?

125

En un RecyclerView, puedo desplazarme repentinamente a la parte superior de un elemento seleccionado usando:

((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(position, 0);

Sin embargo, esto mueve abruptamente el artículo a la posición superior. Quiero pasar a la parte superior de un elemento sin problemas .

También he intentado:

recyclerView.smoothScrollToPosition(position);

pero no funciona bien, ya que no mueve el elemento a la posición seleccionada en la parte superior. Simplemente desplaza la lista hasta que el elemento en la posición sea visible.

Tiago
fuente

Respuestas:

224

RecyclerViewestá diseñado para ser extensible, por lo que no es necesario subclasificar LayoutManager(como sugiere droidev ) solo para realizar el desplazamiento.

En cambio, solo cree un SmoothScrollercon la preferencia SNAP_TO_START:

RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(context) {
  @Override protected int getVerticalSnapPreference() {
    return LinearSmoothScroller.SNAP_TO_START;
  }
};

Ahora establece la posición a la que desea desplazarse:

smoothScroller.setTargetPosition(position);

y pasa ese SmoothScroller al LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Paul Woitaschek
fuente
9
Gracias por esta solución más corta. Solo dos cosas más que tuve que considerar en mi implementación: ya que tengo una vista de desplazamiento horizontal que tuve que configurar protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }. Además, tuve que implementar el método abstracto public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }.
AustrianDude
2
RecyclerView puede estar diseñado para ser extensible, pero una cosa simple como esta simplemente se siente floja de faltar. Buena respuesta aunque! Gracias.
Michael
8
¿Hay alguna manera de dejar que se inicie, pero con un desplazamiento de X dp?
Mark Buikema
2
@Alessio exactamente, esto romperá la funcionalidad predeterminada smoothScrollToPosition de RecyclerView
droidev
44
¿Es posible reducir su velocidad?
Alireza Noorali
112

para esto tienes que crear un LayoutManager personalizado

public class LinearLayoutManagerWithSmoothScroller extends LinearLayoutManager {

    public LinearLayoutManagerWithSmoothScroller(Context context) {
        super(context, VERTICAL, false);
    }

    public LinearLayoutManagerWithSmoothScroller(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
                                       int position) {
        RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
        smoothScroller.setTargetPosition(position);
        startSmoothScroll(smoothScroller);
    }

    private class TopSnappedSmoothScroller extends LinearSmoothScroller {
        public TopSnappedSmoothScroller(Context context) {
            super(context);

        }

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

        @Override
        protected int getVerticalSnapPreference() {
            return SNAP_TO_START;
        }
    }
}

use esto para su RecyclerView y llame a smoothScrollToPosition.

ejemplo:

 recyclerView.setLayoutManager(new LinearLayoutManagerWithSmoothScroller(context));
 recyclerView.smoothScrollToPosition(position);

esto se desplazará hasta la parte superior del elemento RecyclerView de la posición especificada.

espero que esto ayude.

droidev
fuente
Tuve problemas para implementar el LayoutManager sugerido. Esta respuesta aquí me
resultó
3
La única respuesta útil después de horas de dolor, ¡funciona como está diseñado!
wblaschko
1
@droidev He usado tu ejemplo con desplazamiento suave y, en general, SNAP_TO_START es lo que necesito, pero me funciona con algunos problemas. A veces, el desplazamiento se detiene 1,2,3 o 4 posiciones anteriores a las que paso smoothScrollToPosition. ¿Por qué puede ocurrir este problema? Gracias.
Natasha
¿puede de alguna manera comunicarse con su código? Estoy bastante seguro de que es su problema de código.
droidev
1
Esta es la respuesta correcta a pesar de la aceptada
Alessio
25

Esta es una función de extensión que escribí en Kotlin para usar con RecyclerView(basada en la respuesta de @Paul Woitaschek):

fun RecyclerView.smoothSnapToPosition(position: Int, snapMode: Int = LinearSmoothScroller.SNAP_TO_START) {
  val smoothScroller = object : LinearSmoothScroller(this.context) {
    override fun getVerticalSnapPreference(): Int = snapMode
    override fun getHorizontalSnapPreference(): Int = snapMode
  }
  smoothScroller.targetPosition = position
  layoutManager?.startSmoothScroll(smoothScroller)
}

Úselo así:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovahost
fuente
¡Esto funciona! El problema con el desplazamiento suave es cuando tienes listas grandes y luego te desplazas hacia la parte inferior o superior lleva mucho, mucho tiempo
portfoliobuilder
@vovahost, ¿cómo puedo centrar la posición deseada?
ysfcyln
@ysfcyln Consulte esta respuesta stackoverflow.com/a/39654328/1502079
vovahost
12

Podemos intentar así

    recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(), recyclerView.getAdapter().getItemCount());
Supto
fuente
5

Anule la función CalculateDyToMakeVisible / CalculateDxToMakeVisible en LinearSmoothScroller para implementar la posición de desplazamiento Y / X

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
usuario2526517
fuente
Esto es lo que estaba buscando. Gracias por compartir esto para la implementación de la posición de desplazamiento de x / y :)
Shan Xeeshi
3

La forma más fácil que he encontrado para desplazar a RecyclerViewes la siguiente:

// Define the Index we wish to scroll to.
final int lIndex = 0;
// Assign the RecyclerView's LayoutManager.
this.getRecyclerView().setLayoutManager(this.getLinearLayoutManager());
// Scroll the RecyclerView to the Index.
this.getLinearLayoutManager().smoothScrollToPosition(this.getRecyclerView(), new RecyclerView.State(), lIndex);
Mapsy
fuente
2
Esto no se desplazará a una posición si esa posición ya está visible. Se desplaza la menor cantidad necesaria para que la posición sea visible. Entonces, ¿por qué necesitamos el que tiene un desplazamiento especificado?
TatiOverflow
3

Solía ​​así:

recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView, new RecyclerView.State(), 5);
Criss
fuente
2

Gracias, @droidev por la solución. Si alguien busca la solución de Kotlin, consulte esto:

    class LinearLayoutManagerWithSmoothScroller: LinearLayoutManager {
    constructor(context: Context) : this(context, VERTICAL,false)
    constructor(context: Context, orientation: Int, reverseValue: Boolean) : super(context, orientation, reverseValue)

    override fun smoothScrollToPosition(recyclerView: RecyclerView?, state: RecyclerView.State?, position: Int) {
        super.smoothScrollToPosition(recyclerView, state, position)
        val smoothScroller = TopSnappedSmoothScroller(recyclerView?.context)
        smoothScroller.targetPosition = position
        startSmoothScroll(smoothScroller)
    }

    private class TopSnappedSmoothScroller(context: Context?) : LinearSmoothScroller(context){
        var mContext = context
        override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
            return LinearLayoutManagerWithSmoothScroller(mContext as Context)
                    .computeScrollVectorForPosition(targetPosition)
        }

        override fun getVerticalSnapPreference(): Int {
            return SNAP_TO_START
        }


    }

}
PANKAJ KUMAR MAKWANA
fuente
1
Gracias @pankaj, estaba buscando una solución en Kotlin.
Akshay Kumar Ambos
1
  1. Extienda la clase "LinearLayout" y anule las funciones necesarias
  2. Cree una instancia de la clase anterior en su fragmento o actividad
  3. Llame a "recyclerView.smoothScrollToPosition (targetPosition)

CustomLinearLayout.kt:

class CustomLayoutManager(private val context: Context, layoutDirection: Int):
  LinearLayoutManager(context, layoutDirection, false) {

    companion object {
      // This determines how smooth the scrolling will be
      private
      const val MILLISECONDS_PER_INCH = 300f
    }

    override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State, position: Int) {

      val smoothScroller: LinearSmoothScroller = object: LinearSmoothScroller(context) {

        fun dp2px(dpValue: Float): Int {
          val scale = context.resources.displayMetrics.density
          return (dpValue * scale + 0.5f).toInt()
        }

        // change this and the return super type to "calculateDyToMakeVisible" if the layout direction is set to VERTICAL
        override fun calculateDxToMakeVisible(view: View ? , snapPreference : Int): Int {
          return super.calculateDxToMakeVisible(view, SNAP_TO_END) - dp2px(50f)
        }

        //This controls the direction in which smoothScroll looks for your view
        override fun computeScrollVectorForPosition(targetPosition: Int): PointF ? {
          return this @CustomLayoutManager.computeScrollVectorForPosition(targetPosition)
        }

        //This returns the milliseconds it takes to scroll one pixel.
        override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
          return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
        }
      }
      smoothScroller.targetPosition = position
      startSmoothScroll(smoothScroller)
    }
  }

Nota: El ejemplo anterior está configurado en dirección HORIZONTAL, puede pasar VERTICAL / HORIZONTAL durante la inicialización.

Si establece la dirección en VERTICAL , debe cambiar el " CalculateDxToMakeVisible " a " CalculateDyToMakeVisible " (también tenga en cuenta el valor de retorno de la llamada de supertipo)

Activity / Fragment.kt :

...
smoothScrollerLayoutManager = CustomLayoutManager(context, LinearLayoutManager.HORIZONTAL)
recyclerView.layoutManager = smoothScrollerLayoutManager
.
.
.
fun onClick() {
  // targetPosition passed from the adapter to activity/fragment
  recyclerView.smoothScrollToPosition(targetPosition)
}
Ali Akbar
fuente
0

Probablemente el enfoque @droidev sea el correcto, pero solo quiero publicar algo un poco diferente, que básicamente hace el mismo trabajo y no requiere la extensión del LayoutManager.

Una NOTA aquí: esto funcionará bien si su elemento (el que desea desplazar en la parte superior de la lista) está visible en la pantalla y solo desea desplazarlo hacia la parte superior automáticamente. Es útil cuando el último elemento de su lista tiene alguna acción, que agrega nuevos elementos en la misma lista y desea enfocar al usuario en los nuevos elementos agregados:

int recyclerViewTop = recyclerView.getTop();
int positionTop = recyclerView.findViewHolderForAdapterPosition(positionToScroll) != null ? recyclerView.findViewHolderForAdapterPosition(positionToScroll).itemView.getTop() : 200;
final int calcOffset = positionTop - recyclerViewTop; 
//then the actual scroll is gonna happen with (x offset = 0) and (y offset = calcOffset)
recyclerView.scrollBy(0, offset);

La idea es simple: 1. Necesitamos obtener la coordenada superior del elemento de vista del reciclador; 2. Necesitamos obtener la coordenada superior del elemento de vista que queremos desplazar hacia arriba; 3. Al final con el desplazamiento calculado tenemos que hacer

recyclerView.scrollBy(0, offset);

200 es solo un valor entero codificado de ejemplo que puede usar si el elemento del marcador no existe, porque eso también es posible.

Stoycho Andreev
fuente
0

Quiero abordar más a fondo el problema de la duración del desplazamiento , que, si elige cualquier respuesta anterior, de hecho variará dramáticamente (e inaceptablemente) de acuerdo con la cantidad de desplazamiento necesario para alcanzar la posición objetivo desde la posición actual.

Para obtener una duración de desplazamiento uniforme, la velocidad (píxeles por milisegundo) debe tener en cuenta el tamaño de cada elemento individual, y cuando los elementos son de dimensión no estándar, se agrega un nuevo nivel de complejidad.

Esta puede ser la razón por la cual los desarrolladores de RecyclerView implementaron la canasta demasiado dura para este aspecto vital del desplazamiento suave.

Suponiendo que desea una duración de desplazamiento semi uniforme y que su lista contiene elementos semi uniformes , necesitará algo como esto.

/** Smoothly scroll to specified position allowing for interval specification. <br>
 * Note crude deceleration towards end of scroll
 * @param rv        Your RecyclerView
 * @param toPos     Position to scroll to
 * @param duration  Approximate desired duration of scroll (ms)
 * @throws IllegalArgumentException */
private static void smoothScroll(RecyclerView rv, int toPos, int duration) throws IllegalArgumentException {
    int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;     // See androidx.recyclerview.widget.LinearSmoothScroller
    int itemHeight = rv.getChildAt(0).getHeight();  // Height of first visible view! NB: ViewGroup method!
    itemHeight = itemHeight + 33;                   // Example pixel Adjustment for decoration?
    int fvPos = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
    int i = Math.abs((fvPos - toPos) * itemHeight);
    if (i == 0) { i = (int) Math.abs(rv.getChildAt(0).getY()); }
    final int totalPix = i;                         // Best guess: Total number of pixels to scroll
    RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(rv.getContext()) {
        @Override protected int getVerticalSnapPreference() {
            return LinearSmoothScroller.SNAP_TO_START;
        }
        @Override protected int calculateTimeForScrolling(int dx) {
            int ms = (int) ( duration * dx / (float)totalPix );
            // Now double the interval for the last fling.
            if (dx < TARGET_SEEK_SCROLL_DISTANCE_PX ) { ms = ms*2; } // Crude deceleration!
            //lg(format("For dx=%d we allot %dms", dx, ms));
            return ms;
        }
    };
    //lg(format("Total pixels from = %d to %d = %d [ itemHeight=%dpix ]", fvPos, toPos, totalPix, itemHeight));
    smoothScroller.setTargetPosition(toPos);
    rv.getLayoutManager().startSmoothScroll(smoothScroller);
}

PD: Maldigo el día que comencé a convertir indiscriminadamente ListView a RecyclerView .

Mal perdedor
fuente
0

Puede revertir su lista list.reverse()y finalmente llamarRecylerView.scrollToPosition(0)

    list.reverse()
    layout = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,true)  
    RecylerView.scrollToPosition(0)
Thanh Vu
fuente