RecyclerView - Как сгладить прокрутку вверх элемента в определенной позиции?

126

В RecyclerView я могу внезапно прокрутить к началу выбранного элемента, используя:

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

Однако это резко перемещает элемент в верхнее положение. Я хочу плавно перейти к началу элемента .

Я также пробовал:

recyclerView.smoothScrollToPosition(position);

но он не работает, так как не перемещает элемент в позицию, выбранную наверху. Он просто прокручивает список, пока не станет виден элемент в позиции.

Tiago
источник

Ответы:

225

RecyclerViewразработан с возможностью расширения, поэтому нет необходимости создавать подкласс LayoutManager(как предлагал droidev ) только для выполнения прокрутки.

Вместо этого просто создайте SmoothScrollerс предпочтением SNAP_TO_START:

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

Теперь вы устанавливаете позицию, в которую хотите прокрутить:

smoothScroller.setTargetPosition(position);

и передайте этот SmoothScroller в LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Пол Войташек
источник
9
Спасибо за это более короткое решение. Еще две вещи, которые я должен был учитывать в своей реализации: поскольку у меня есть горизонтальная прокрутка, я должен был установить protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }. Кроме того, мне пришлось реализовать абстрактный метод public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }.
AustrianDude
2
RecyclerView может быть спроектирован так, чтобы быть расширяемым, но такой простой вещи просто лень упускать. Хороший ответ! Спасибо.
Michael
8
Есть ли способ, чтобы он запустился, но со смещением X dp?
Марк Буйкема
5
можно ли замедлить его скорость?
Алиреза Нурали
3
Также интересно, можно ли изменить скорость, с которой происходит прокрутка (медленнее) @AlirezaNoorali
vikzilla
112

для этого вам нужно создать собственный LayoutManager

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

используйте это для своего RecyclerView и вызовите smoothScrollToPosition.

пример :

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

это приведет к прокрутке вверх элемента RecyclerView указанной позиции.

надеюсь это поможет.

droidev
источник
У меня возникли проблемы с реализацией предложенного LayoutManager. Этот ответ здесь сработал для меня намного проще: stackoverflow.com/questions/28025425/…
arberg
3
Единственный полезный ответ после нескольких часов боли, это работает так, как задумано!
wblaschko
1
@droidv Я использовал ваш пример с плавной прокруткой, и обычно мне нужен SNAP_TO_START, но он работает с некоторыми проблемами для меня. Иногда прокрутка останавливается на 1,2,3 или 4 позиции раньше, чем я перехожу smoothScrollToPosition. Почему может возникнуть эта проблема? Спасибо.
Наташа
Вы можете как-нибудь передать нам свой код? Я почти уверен, что это проблема вашего кода.
droidev 02
1
Это правильный ответ, несмотря на принятый
Алессио
26

Это функция расширения, которую я написал на Kotlin для использования с RecyclerView(на основе ответа @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)
}

Используйте это так:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovahost
источник
Это работает! Проблема с плавной прокруткой заключается в том, что у вас есть большие списки, а затем прокрутка вниз или вверх занимает много времени,
портфолио,
@vovahost, как я могу центрировать желаемую позицию?
ysfcyln
@ysfcyln Проверьте этот ответ stackoverflow.com/a/39654328/1502079
vovahost
12

Мы можем попробовать вот так

    recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(), recyclerView.getAdapter().getItemCount());
Supto
источник
5

Переопределите функцию calculateDyToMakeVisible / calculateDxToMakeVisible в LinearSmoothScroller, чтобы реализовать смещение позиции Y / X

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
user2526517
источник
Это то, что я искал. Спасибо, что поделились этим для реализации смещения положения x / y :)
Shan Xeeshi
3

Самый простой способ прокрутки, который я нашел, RecyclerViewзаключается в следующем:

// 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
источник
2
Это не будет прокручиваться до позиции, если эта позиция уже видна. Он прокручивает наименьшее количество прокрутки, необходимое для отображения позиции. Итак, зачем нам нужен тот, у которого указано смещение.
TatiOverflow
3

Я использовал вот так:

recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView, new RecyclerView.State(), 5);
Criss
источник
2

Спасибо, @droidv за решение. Если кто-то ищет решение Kotlin, обратитесь к этому:

    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
        }


    }

}
ПАНКАДЖ КУМАР МАКВАНА
источник
1
Спасибо @pankaj, искал решение в Котлине.
Акшай Кумар Оба
1
  1. Расширить класс LinearLayout и переопределить необходимые функции.
  2. Создайте экземпляр вышеуказанного класса в своем фрагменте или действии
  3. Вызвать 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)
    }
  }

Примечание. В приведенном выше примере задано ГОРИЗОНТАЛЬНОЕ направление, вы можете указать ВЕРТИКАЛЬНОЕ / ГОРИЗОНТАЛЬНОЕ во время инициализации.

Если вы установите направление на ВЕРТИКАЛЬНОЕ, вы должны изменить « calculateDxToMakeVisible » на « calculateDyToMakeVisible » (также обратите внимание на возвращаемое значение вызова супертипа)

Activity / Fragment.kt :

...
smoothScrollerLayoutManager = CustomLayoutManager(context, LinearLayoutManager.HORIZONTAL)
recyclerView.layoutManager = smoothScrollerLayoutManager
.
.
.
fun onClick() {
  // targetPosition passed from the adapter to activity/fragment
  recyclerView.smoothScrollToPosition(targetPosition)
}
Али Акбар
источник
0

Вероятно, подход @droidv является правильным, но я просто хочу опубликовать что-то немного другое, которое в основном выполняет ту же работу и не требует расширения LayoutManager.

ПРИМЕЧАНИЕ здесь - это будет работать хорошо, если ваш элемент (тот, который вы хотите прокрутить в верхней части списка) отображается на экране, и вы просто хотите автоматически прокрутить его вверх. Это полезно, когда последний элемент в вашем списке имеет какое-либо действие, которое добавляет новые элементы в тот же список, и вы хотите сосредоточить внимание пользователя на новых добавленных элементах:

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

Идея проста: 1. Нам нужно получить координату вершины элемента recyclerview; 2. Нам нужно получить верхнюю координату элемента просмотра, который мы хотим прокрутить вверх; 3. В конце с рассчитанным смещением нам нужно сделать

recyclerView.scrollBy(0, offset);

200 - это просто пример жестко закодированного целочисленного значения, которое вы можете использовать, если элемент держателя просмотра не существует, потому что это тоже возможно.

Стойчо Андреев
источник
0

Я хочу более полно рассмотреть вопрос о продолжительности прокрутки , которая, если вы выберете какой-либо более ранний ответ, на самом деле будет сильно (и неприемлемо) варьироваться в зависимости от количества прокрутки, необходимого для достижения целевой позиции из текущей позиции.

Чтобы получить единообразную продолжительность прокрутки, скорость (пикселей в миллисекунду) должна учитывать размер каждого отдельного элемента, а когда элементы нестандартного размера, добавляется совершенно новый уровень сложности.

Возможно, поэтому разработчики RecyclerView развернули слишком жесткую корзину для этого жизненно важного аспекта плавной прокрутки.

Предполагая, что вам нужна полуравномерная продолжительность прокрутки и что ваш список содержит полуоднородные элементы, вам понадобится что-то вроде этого.

/** 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: Проклинаю тот день, когда я начал без разбора преобразовывать ListView в RecyclerView .

Плохой неудачник
источник
-1

Вы можете перевернуть свой список list.reverse()и, наконец, позвонитьRecylerView.scrollToPosition(0)

    list.reverse()
    layout = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,true)  
    RecylerView.scrollToPosition(0)
Тхань Ву
источник