Быстрая прокрутка в RecyclerView

Я пытаюсь использовать новый класс RecyclerView для сценария, где я хочу, чтобы компонент привязывался к определенному элементу при прокрутке (в качестве примера такого списка с элементом с центральной блокировкой приходит в голову старая Gallery Android).

Это подход, который я принимаю до сих пор:

У меня есть интерфейс ISnappyLayoutManager , который содержит метод getPositionForVelocity , который вычисляет, в какой позиции представление должно заканчивать прокрутку с учетом начальной скорости движения.

 public interface ISnappyLayoutManager { int getPositionForVelocity(int velocityX, int velocityY); } 

Затем у меня есть класс SnappyRecyclerView , который подклассирует RecyclerView и переопределяет метод SnappyRecyclerView () таким образом, чтобы отображать точное количество точно:

 public final class SnappyRecyclerView extends RecyclerView { /** other methods deleted **/ @Override public boolean fling(int velocityX, int velocityY) { LayoutManager lm = getLayoutManager(); if (lm instanceof ISnappyLayoutManager) { super.smoothScrollToPosition(((ISnappyLayoutManager) getLayoutManager()) .getPositionForVelocity(velocityX, velocityY)); } return true; } } 

Я не очень доволен этим подходом по нескольким причинам. Прежде всего, похоже, что философия «RecyclerView» должна подклассифицировать ее для реализации определенного типа прокрутки. Во-вторых, если я хочу просто использовать LinearLayoutManager по умолчанию, это становится несколько сложным, так как мне приходится возиться со своими внутренностями, чтобы понять его текущее состояние прокрутки и рассчитать, где именно это прокручивается. Наконец, это даже не позаботится обо всех возможных сценариях прокрутки, как будто вы перемещаете список, а затем приостанавливаете, а затем поднимаете палец, не происходит никакого события перехода (скорость слишком низкая), и поэтому список остается на полпути должность. Об этом можно позаботиться, добавив прослушиватель состояния прокрутки в RecyclerView , но это также очень хладнокровно.

Я чувствую, что мне что-то не хватает. Есть лучший способ сделать это?

Solutions Collecting From Web of "Быстрая прокрутка в RecyclerView"

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

Основами реализации являются: Прокрутка в RecyclerView – это своего рода разделение между RecyclerView и LinearLayoutManager . Мне нужно обрабатывать два случая:

  1. Пользователь бросает вид. Поведение по умолчанию заключается в том, что RecyclerView передает сброс на внутренний Scroller который затем выполняет прокрутку магии. Это проблематично, потому что RecyclerView обычно располагается в незанятой позиции. Я решаю это, переопределяя реализацию RetyclerView LinearLayoutManager fling() и вместо того, чтобы бросать, плавно LinearLayoutManager в позицию.
  2. Пользователь поднимает палец с недостаточной скоростью, чтобы инициировать свиток. В этом случае не происходит перелета. Я хочу обнаружить этот случай в том случае, если представление не находится в позиции привязки. Я делаю это, переопределяя метод onTouchEvent .

SnappyRecyclerView :

 public final class SnappyRecyclerView extends RecyclerView { public SnappyRecyclerView(Context context) { super(context); } public SnappyRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); } public SnappyRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean fling(int velocityX, int velocityY) { final LayoutManager lm = getLayoutManager(); if (lm instanceof ISnappyLayoutManager) { super.smoothScrollToPosition(((ISnappyLayoutManager) getLayoutManager()) .getPositionForVelocity(velocityX, velocityY)); return true; } return super.fling(velocityX, velocityY); } @Override public boolean onTouchEvent(MotionEvent e) { // We want the parent to handle all touch events--there's a lot going on there, // and there is no reason to overwrite that functionality--bad things will happen. final boolean ret = super.onTouchEvent(e); final LayoutManager lm = getLayoutManager(); if (lm instanceof ISnappyLayoutManager && (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) && getScrollState() == SCROLL_STATE_IDLE) { // The layout manager is a SnappyLayoutManager, which means that the // children should be snapped to a grid at the end of a drag or // fling. The motion event is either a user lifting their finger or // the cancellation of a motion events, so this is the time to take // over the scrolling to perform our own functionality. // Finally, the scroll state is idle--meaning that the resultant // velocity after the user's gesture was below the threshold, and // no fling was performed, so the view may be in an unaligned state // and will not be flung to a proper state. smoothScrollToPosition(((ISnappyLayoutManager) lm).getFixScrollPos()); } return ret; } } 

Интерфейс для быстрых менеджеров макетов:

 /** * An interface that LayoutManagers that should snap to grid should implement. */ public interface ISnappyLayoutManager { /** * @param velocityX * @param velocityY * @return the resultant position from a fling of the given velocity. */ int getPositionForVelocity(int velocityX, int velocityY); /** * @return the position this list must scroll to to fix a state where the * views are not snapped to grid. */ int getFixScrollPos(); } 

И вот пример LayoutManager который подклассифицирует LinearLayoutManager чтобы создать LayoutManager с плавной прокруткой:

 public class SnappyLinearLayoutManager extends LinearLayoutManager implements ISnappyLayoutManager { // These variables are from android.widget.Scroller, which is used, via ScrollerCompat, by // Recycler View. The scrolling distance calculation logic originates from the same place. Want // to use their variables so as to approximate the look of normal Android scrolling. // Find the Scroller fling implementation in android.widget.Scroller.fling(). private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); private static double FRICTION = 0.84; private double deceleration; public SnappyLinearLayoutManager(Context context) { super(context); calculateDeceleration(context); } public SnappyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); calculateDeceleration(context); } private void calculateDeceleration(Context context) { deceleration = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.3700787 // inches per meter // pixels per inch. 160 is the "default" dpi, ie one dip is one pixel on a 160 dpi // screen * context.getResources().getDisplayMetrics().density * 160.0f * FRICTION; } @Override public int getPositionForVelocity(int velocityX, int velocityY) { if (getChildCount() == 0) { return 0; } if (getOrientation() == HORIZONTAL) { return calcPosForVelocity(velocityX, getChildAt(0).getLeft(), getChildAt(0).getWidth(), getPosition(getChildAt(0))); } else { return calcPosForVelocity(velocityY, getChildAt(0).getTop(), getChildAt(0).getHeight(), getPosition(getChildAt(0))); } } private int calcPosForVelocity(int velocity, int scrollPos, int childSize, int currPos) { final double dist = getSplineFlingDistance(velocity); final double tempScroll = scrollPos + (velocity > 0 ? dist : -dist); if (velocity < 0) { // Not sure if I need to lower bound this here. return (int) Math.max(currPos + tempScroll / childSize, 0); } else { return (int) (currPos + (tempScroll / childSize) + 1); } } @Override public void smoothScrollToPosition(RecyclerView recyclerView, State state, int position) { final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { // I want a behavior where the scrolling always snaps to the beginning of // the list. Snapping to end is also trivial given the default implementation. // If you need a different behavior, you may need to override more // of the LinearSmoothScrolling methods. protected int getHorizontalSnapPreference() { return SNAP_TO_START; } protected int getVerticalSnapPreference() { return SNAP_TO_START; } @Override public PointF computeScrollVectorForPosition(int targetPosition) { return SnappyLinearLayoutManager.this .computeScrollVectorForPosition(targetPosition); } }; linearSmoothScroller.setTargetPosition(position); startSmoothScroll(linearSmoothScroller); } private double getSplineFlingDistance(double velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return ViewConfiguration.getScrollFriction() * deceleration * Math.exp(DECELERATION_RATE / decelMinusOne * l); } private double getSplineDeceleration(double velocity) { return Math.log(INFLEXION * Math.abs(velocity) / (ViewConfiguration.getScrollFriction() * deceleration)); } /** * This implementation obviously doesn't take into account the direction of the * that preceded it, but there is no easy way to get that information without more * hacking than I was willing to put into it. */ @Override public int getFixScrollPos() { if (this.getChildCount() == 0) { return 0; } final View child = getChildAt(0); final int childPos = getPosition(child); if (getOrientation() == HORIZONTAL && Math.abs(child.getLeft()) > child.getMeasuredWidth() / 2) { // Scrolled first view more than halfway offscreen return childPos + 1; } else if (getOrientation() == VERTICAL && Math.abs(child.getTop()) > child.getMeasuredWidth() / 2) { // Scrolled first view more than halfway offscreen return childPos + 1; } return childPos; } } 

С LinearSnapHelper это сейчас очень просто.

Все, что вам нужно сделать, это:

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

Это так просто! Обратите внимание, что LinearSnapHelper был добавлен в библиотеку поддержки, начиная с версии 24.2.0.

Это означает, что вы должны добавить это в свой модуль приложения build.gradle

 compile "com.android.support:recyclerview-v7:24.2.0" 

Мне удалось найти более чистый способ сделать это. @Catherine (OP) сообщите мне, если это можно улучшить, или вы чувствуете, что это улучшение по сравнению с вашими 🙂

Вот прослушиватель прокрутки, который я использую.

https://github.com/humblerookie/centerlockrecyclerview/

Я опустил некоторые незначительные допущения здесь, например, например.

1) Исходные и заключительные прокладки : первый и последний элементы в горизонтальном прокрутке должны иметь начальные и конечные прокладки, соответственно установленные таким образом, чтобы начальный и конечный виды находились в центре при прокрутке до первого и последнего соответственно. Например, в onBindViewHolder вы могли сделать что-то вроде этого.

 @Override public void onBindViewHolder(ReviewHolder holder, int position) { holder.container.setPadding(0,0,0,0);//Resetpadding if(position==0){ //Only one element if(mData.size()==1){ holder.container.setPadding(totalpaddinginit/2,0,totalpaddinginit/2,0); } else{ //>1 elements assign only initpadding holder.container.setPadding(totalpaddinginit,0,0,0); } } else if(position==mData.size()-1){ holder.container.setPadding(0,0,totalpaddingfinal,0); } } public class ReviewHolder extends RecyclerView.ViewHolder { protected TextView tvName; View container; public ReviewHolder(View itemView) { super(itemView); container=itemView; tvName= (TextView) itemView.findViewById(R.id.text); } } 

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

Любое лицо сталкивается с проблемой любезного комментария.

Мне также нужен был быстрый просмотр ресайклеров. Я хочу, чтобы элемент просмотра ресайклера привязывался слева от столбца. Это привело к внедрению SnapScrollListener, который я установил в представлении recycler. Это мой код:

SnapScrollListener:

 class SnapScrollListener extends RecyclerView.OnScrollListener { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (RecyclerView.SCROLL_STATE_IDLE == newState) { final int scrollDistance = getScrollDistanceOfColumnClosestToLeft(mRecyclerView); if (scrollDistance != 0) { mRecyclerView.smoothScrollBy(scrollDistance, 0); } } } } 

Расчет привязки:

 private int getScrollDistanceOfColumnClosestToLeft(final RecyclerView recyclerView) { final LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); final RecyclerView.ViewHolder firstVisibleColumnViewHolder = recyclerView.findViewHolderForAdapterPosition(manager.findFirstVisibleItemPosition()); if (firstVisibleColumnViewHolder == null) { return 0; } final int columnWidth = firstVisibleColumnViewHolder.itemView.getMeasuredWidth(); final int left = firstVisibleColumnViewHolder.itemView.getLeft(); final int absoluteLeft = Math.abs(left); return absoluteLeft <= (columnWidth / 2) ? left : columnWidth - absoluteLeft; } 

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

Установка слушателя:

 mRecyclerView.addOnScrollListener(new SnapScrollListener()); 

Вот более простой взлом для плавной прокрутки до определенной позиции в событии сбрасывания:

 @Override public boolean fling(int velocityX, int velocityY) { smoothScrollToPosition(position); return super.fling(0, 0); } 

Переопределите метод fling с вызовом smoothScrollToPosition (int position), где «int position» – позиция, которую вы хотите видеть в адаптере. Вам нужно каким-то образом получить значение позиции, но это зависит от ваших потребностей и реализации.

Очень простой подход для достижения привязки к положению –

  recyclerView.setOnScrollListener(new OnScrollListener() { private boolean scrollingUp; @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { // Or use dx for horizontal scrolling scrollingUp = dy < 0; } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { // Make sure scrolling has stopped before snapping if (newState == RecyclerView.SCROLL_STATE_IDLE) { // layoutManager is the recyclerview's layout manager which you need to have reference in advance int visiblePosition = scrollingUp ? layoutManager.findFirstVisibleItemPosition() : layoutManager.findLastVisibleItemPosition(); int completelyVisiblePosition = scrollingUp ? layoutManager .findFirstCompletelyVisibleItemPosition() : layoutManager .findLastCompletelyVisibleItemPosition(); // Check if we need to snap if (visiblePosition != completelyVisiblePosition) { recyclerView.smoothScrollToPosition(visiblePosition); return; } } }); 

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

Я применил рабочее решение для горизонтальной ориентации RecyclerView, которое просто считывает координаты onTouchEvent, сначала MOVE и UP. На UP вычислите позицию, в которой нам нужно перейти.

 public final class SnappyRecyclerView extends RecyclerView { private Point mStartMovePoint = new Point( 0, 0 ); private int mStartMovePositionFirst = 0; private int mStartMovePositionSecond = 0; public SnappyRecyclerView( Context context ) { super( context ); } public SnappyRecyclerView( Context context, AttributeSet attrs ) { super( context, attrs ); } public SnappyRecyclerView( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); } @Override public boolean onTouchEvent( MotionEvent e ) { final boolean ret = super.onTouchEvent( e ); final LayoutManager lm = getLayoutManager(); View childView = lm.getChildAt( 0 ); View childViewSecond = lm.getChildAt( 1 ); if( ( e.getAction() & MotionEvent.ACTION_MASK ) == MotionEvent.ACTION_MOVE && mStartMovePoint.x == 0) { mStartMovePoint.x = (int)e.getX(); mStartMovePoint.y = (int)e.getY(); mStartMovePositionFirst = lm.getPosition( childView ); if( childViewSecond != null ) mStartMovePositionSecond = lm.getPosition( childViewSecond ); }// if MotionEvent.ACTION_MOVE if( ( e.getAction() & MotionEvent.ACTION_MASK ) == MotionEvent.ACTION_UP ){ int currentX = (int)e.getX(); int width = childView.getWidth(); int xMovement = currentX - mStartMovePoint.x; // move back will be positive value final boolean moveBack = xMovement > 0; int calculatedPosition = mStartMovePositionFirst; if( moveBack && mStartMovePositionSecond > 0 ) calculatedPosition = mStartMovePositionSecond; if( Math.abs( xMovement ) > ( width / 3 ) ) calculatedPosition += moveBack ? -1 : 1; if( calculatedPosition >= getAdapter().getItemCount() ) calculatedPosition = getAdapter().getItemCount() -1; if( calculatedPosition < 0 || getAdapter().getItemCount() == 0 ) calculatedPosition = 0; mStartMovePoint.x = 0; mStartMovePoint.y = 0; mStartMovePositionFirst = 0; mStartMovePositionSecond = 0; smoothScrollToPosition( calculatedPosition ); }// if MotionEvent.ACTION_UP return ret; }} 

Отлично работает для меня, дайте мне знать, если что-то не так.

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

https://gist.github.com/lauw/fc84f7d04f8c54e56d56

Он поддерживает только горизонтальные рециркуляции и привязки к центру, а также может масштабировать представления, исходя из того, насколько они далеко от центра. Используйте в качестве замены RecyclerView.

Редактировать: 08/2016 Сделано в репозитории:
https://github.com/lauw/Android-SnappingRecyclerView
Я просто продолжу это, работая над лучшей реализацией.

Чтобы обновить ответ humblerookie:

Этот прослушиватель прокрутки действительно эффективен для централизованного блокирования https://github.com/humblerookie/centerlockrecyclerview/

Но вот более простой способ добавить отступы в начале и конце recyclerview для центрирования его элементов:

 mRecycler.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int childWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CHILD_WIDTH_IN_DP, getResources().getDisplayMetrics()); int offset = (mRecycler.getWidth() - childWidth) / 2; mRecycler.setPadding(offset, mRecycler.getPaddingTop(), offset, mRecycler.getPaddingBottom()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mRecycler.getViewTreeObserver().removeOnGlobalLayoutListener(this); } else { mRecycler.getViewTreeObserver().removeGlobalOnLayoutListener(this); } } }); 

Если вам нужна привязка для начала, вершины, конца или снизу, используйте GravitySnapHelper ( https://github.com/rubensousa/RecyclerViewSnap/blob/master/app/src/main/java/com/github/rubensousa/recyclerviewsnap/GravitySnapHelper .java ).

Центр привязки:

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

Захват начинается с GravitySnapHelper:

 startRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); SnapHelper snapHelperStart = new GravitySnapHelper(Gravity.START); snapHelperStart.attachToRecyclerView(startRecyclerView); 

Snapping top с GravitySnapHelper:

 topRecyclerView.setLayoutManager(new LinearLayoutManager(this)); SnapHelper snapHelperTop = new GravitySnapHelper(Gravity.TOP); snapHelperTop.attachToRecyclerView(topRecyclerView); 

И еще один вариант очистки – использовать пользовательский LayoutManager , вы можете проверить https://github.com/apptik/multiview/tree/master/layoutmanagers

Он находится в разработке, но работает достаточно хорошо. Доступна моментальная копия: https://oss.sonatype.org/content/repositories/snapshots/io/apptik/multiview/layoutmanagers/

Пример:

 recyclerView.setLayoutManager(new SnapperLinearLayoutManager(getActivity())); 

Мне нужно что-то немного отличающееся от всех вышеперечисленных ответов.

Основными требованиями были:

  1. Он работает так же, когда пользователь бросает или просто выпускает свой палец.
  2. Использует собственный механизм прокрутки, чтобы иметь такое же «чувство», как обычный RecyclerView .
  3. Когда он останавливается, он начинает плавно прокручиваться до ближайшей точки привязки.
  4. Нет необходимости использовать пользовательские LayoutManager или RecyclerView . Просто RecyclerView.OnScrollListener , который затем прикрепляется recyclerView.addOnScrollListener(snapScrollListener) . Таким образом, код намного чище.

И два очень специфических требования, которые должны быть легко изменены в приведенном ниже примере, чтобы соответствовать вашему делу:

  1. Работает горизонтально.
  2. Привязывает левый край элемента к определенной точке в RecyclerView .

Это решение использует собственный LinearSmoothScroller . Разница заключается в том, что на последнем этапе, когда найден «целевой вид», он меняет расчет смещения так, чтобы он привязывался к определенной позиции.

 public class SnapScrollListener extends RecyclerView.OnScrollListener { private static final float MILLIS_PER_PIXEL = 200f; /** The x coordinate of recycler view to which the items should be scrolled */ private final int snapX; int prevState = RecyclerView.SCROLL_STATE_IDLE; int currentState = RecyclerView.SCROLL_STATE_IDLE; public SnapScrollListener(int snapX) { this.snapX = snapX; } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); currentState = newState; if(prevState != RecyclerView.SCROLL_STATE_IDLE && currentState == RecyclerView.SCROLL_STATE_IDLE ){ performSnap(recyclerView); } prevState = currentState; } private void performSnap(RecyclerView recyclerView) { for( int i = 0 ;i < recyclerView.getChildCount() ; i ++ ){ View child = recyclerView.getChildAt(i); final int left = child.getLeft(); int right = child.getRight(); int halfWidth = (right - left) / 2; if (left == snapX) return; if (left - halfWidth <= snapX && left + halfWidth >= snapX) { //check if child is over the snapX position int adapterPosition = recyclerView.getChildAdapterPosition(child); int dx = snapX - left; smoothScrollToPositionWithOffset(recyclerView, adapterPosition, dx); return; } } } private void smoothScrollToPositionWithOffset(RecyclerView recyclerView, int adapterPosition, final int dx) { final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if( layoutManager instanceof LinearLayoutManager) { LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { @Override public PointF computeScrollVectorForPosition(int targetPosition) { return ((LinearLayoutManager) layoutManager).computeScrollVectorForPosition(targetPosition); } @Override protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); final int distance = (int) Math.sqrt(dx * dx + dy * dy); final int time = calculateTimeForDeceleration(distance); if (time > 0) { action.update(-dx, -dy, time, mDecelerateInterpolator); } } @Override protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return MILLIS_PER_PIXEL / displayMetrics.densityDpi; } }; scroller.setTargetPosition(adapterPosition); layoutManager.startSmoothScroll(scroller); } }