RecyclerView - Como suavizar a rolagem para o topo do item em uma determinada posição?

125

Em um RecyclerView, sou capaz de rolar de repente para a parte superior de um item selecionado usando:

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

No entanto, isso move abruptamente o item para a posição superior. Quero passar para o topo de um item sem problemas .

Eu também tentei:

recyclerView.smoothScrollToPosition(position);

mas não funciona bem, pois não move o item para a posição selecionada para o topo. Simplesmente rola a lista até o item na posição ficar visível.

Tiago
fonte

Respostas:

224

RecyclerViewfoi projetado para ser extensível, portanto, não há necessidade de subclassificar o LayoutManager(como sugerido pelo droidev ) apenas para executar a rolagem.

Em vez disso, basta criar um SmoothScrollercom a preferência SNAP_TO_START:

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

Agora você define a posição em que deseja rolar:

smoothScroller.setTargetPosition(position);

e passe esse SmoothScroller para o LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Paul Woitaschek
fonte
9
Obrigado por esta solução mais curta. Apenas mais duas coisas que tive que considerar em minha implementação: como tenho uma exibição de rolagem horizontal, tive que definir protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }. Além disso, eu tive que implementar o método abstrato public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }.
AustrianDude
2
O RecyclerView pode ser projetado para ser extensível, mas algo simples como esse parece preguiçoso por estar faltando. Boa resposta tho! Obrigado.
Michael
8
Existe alguma maneira de deixá-lo começar, mas com um deslocamento de X dp?
Mark Buikema
2
@Alessio exatamente, isso quebrará a funcionalidade smoothScrollToPosition padrão do RecyclerView
droidev
4
é possível diminuir a velocidade?
Alireza Noorali
112

para isso, você deve criar um 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 isso para o seu RecyclerView e chame smoothScrollToPosition.

exemplo:

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

isso rolará para o topo do item RecyclerView da posição especificada.

espero que isto ajude.

droidev
fonte
Ocorreu um problema ao implementar o LayoutManager sugerido. Esta resposta aqui funcionou muito mais fácil para mim: stackoverflow.com/questions/28025425/…
arberg
3
Única resposta útil após horas de dor, isso funciona como projetado!
wblaschko
1
@droidev Usei seu exemplo com rolagem suave e geralmente é o que eu preciso em SNAP_TO_START, mas funciona com alguns problemas para mim. Às vezes, a rolagem para 1,2,3 ou 4 posições anteriores para as quais passo smoothScrollToPosition. Por que esse problema pode ocorrer? Obrigado.
Natasha
você pode, de alguma forma, acessar seu código para nós? Tenho certeza de que é seu problema de código.
Droidev 2/11
1
Esta é a resposta correta, apesar da aceita.
Alessio
25

Esta é uma função de extensão que escrevi no Kotlin para usar com a RecyclerView(com base na resposta 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)
}

Use-o assim:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovahost
fonte
Isso funciona! O problema com rolagem suave é quando você tem grandes listas e, em seguida, percorrer a parte inferior ou superior leva um longo, longo tempo
portfoliobuilder
@vovahost como posso centralizar a posição desejada?
ysfcyln 27/01
@ysfcyln Verifique esta stackoverflow.com/a/39654328/1502079 resposta
vovahost 27/01
12

Podemos tentar assim

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

Substitua a função calculDyToMakeVisible / calculDxToMakeVisible em LinearSmoothScroller para implementar a posição Y / X de deslocamento

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
user2526517
fonte
Era isso que eu estava procurando. Obrigado por compartilhar este para a implementação da posição de deslocamento de x / y :)
Shan Xeeshi
3

A maneira mais fácil que encontrei de rolar a RecyclerViewé a seguinte:

// 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
fonte
2
Isso não rolará para uma posição se essa posição já estiver visível. Rola a quantidade mínima necessária para tornar a posição visível. Portanto, por que precisamos daquele com deslocamento especificado.
TatiOverflow 26/08/19
3

eu usei assim:

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

Obrigado, @droidev pela solução. Se alguém procurando a solução Kotlin, consulte o seguinte:

    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
fonte
1
Obrigado @pankaj, estava procurando uma solução no Kotlin.
Akshay Kumar Ambos
1
  1. Estenda a classe "LinearLayout" e substitua as funções necessárias
  2. Crie uma instância da classe acima em seu fragmento ou atividade
  3. Chame "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: O exemplo acima está definido na direção HORIZONTAL, você pode passar VERTICAL / HORIZONTAL durante a inicialização.

Se você definir a direção como VERTICAL , altere " CalculateDxToMakeVisible " para " CalculeDyToMakeVisible " (lembre-se também do valor de retorno de chamada 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
fonte
0

Provavelmente, a abordagem @droidev é a correta, mas eu só quero publicar algo um pouco diferente, que basicamente faz o mesmo trabalho e não requer extensão do LayoutManager.

Uma NOTA aqui - isso funcionará bem se o seu item (aquele que você deseja rolar no topo da lista) estiver visível na tela e você quiser apenas rolar para o topo automaticamente. É útil quando o último item da sua lista tem alguma ação, que adiciona novos itens à mesma lista e você deseja focar o usuário nos novos itens adicionados:

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

A idéia é simples: 1. Precisamos obter a coordenada superior do elemento de revisão de reciclagem; 2. Precisamos obter a coordenada superior do item de exibição que queremos rolar para o topo; 3. No final, com o deslocamento calculado, precisamos fazer

recyclerView.scrollBy(0, offset);

200 é apenas um exemplo de valor inteiro codificado que você pode usar se o item do visualizador não existir, porque isso também é possível.

Stoycho Andreev
fonte
0

Quero abordar mais detalhadamente a questão da duração da rolagem , que, se você escolher uma resposta anterior, variará de maneira dramática (e inaceitável) de acordo com a quantidade de rolagem necessária para alcançar a posição de destino a partir da posição atual.

Para obter uma duração de rolagem uniforme, a velocidade (pixels por milissegundo) deve ser responsável pelo tamanho de cada item individual - e quando os itens tiverem dimensões fora do padrão, será adicionado um novo nível de complexidade.

Pode ser por isso que os desenvolvedores do RecyclerView implantaram a cesta muito difícil para esse aspecto vital da rolagem suave.

Supondo que você queira uma duração de rolagem semi-uniforme e que sua lista contenha itens semi-uniformes , será necessário algo como isto.

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

PS: Maldito seja o dia em que comecei a converter indiscriminadamente o ListView em RecyclerView .

Mau perdedor
fonte
0

Você pode reverter sua lista list.reverse()e, finalmente, ligarRecylerView.scrollToPosition(0)

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