Кнопка горизонтальной прокрутки RecyclerView в центре

89

Я пытаюсь создать здесь вид карусели, используя RecyclerView, я хочу, чтобы элемент щелкал по середине экрана при прокрутке, по одному элементу за раз. Я пробовал использоватьrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

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

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

Свайп справа налево теперь работает нормально, но не наоборот, что мне здесь не хватает?

Ахмед Авад
источник

Ответы:

161

Теперь LinearSnapHelperэто очень просто.

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

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

Обновить

Доступно с 25.1.0, PagerSnapHelperможет помочь добиться ViewPagerподобного эффекта. Используйте его, как если бы вы использовали LinearSnapHelper.

Старый способ обхода:

Если вы хотите, чтобы он вел себя аналогично ViewPager, попробуйте вместо этого:

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

Приведенная выше реализация просто возвращает позицию рядом с текущим элементом (по центру) в зависимости от направления скорости, независимо от величины.

Первый - это решение от первого лица, включенное в библиотеку поддержки версии 24.2.0. Это означает, что вам нужно добавить это в модуль вашего приложения build.gradleили обновить его.

compile "com.android.support:recyclerview-v7:24.2.0"
ослепить
источник
Это не сработало для меня (но решение @Albert Vila Calvo сработало). Вы реализовали это? Должны ли мы переопределить из него методы? Я думаю, что класс должен делать всю работу из коробки
galaxigirl
Да, отсюда и ответ. Однако я не тестировал полностью. Для ясности, я сделал это с горизонталью, LinearLayoutManagerгде все виды были стандартного размера. Больше ничего, кроме приведенного выше фрагмента, не требуется.
razzledazzle
@galaxigirl извиняюсь, я понял, что вы имели в виду, и внес некоторые изменения, подтверждающие это, пожалуйста, оставьте комментарий. Это альтернативное решение для LinearSnapHelper.
razzledazzle
Это сработало для меня. @galaxigirl, переопределение этого метода, помогает в том, что в противном случае помощник линейной привязки будет привязан к ближайшему элементу при установке прокрутки, а не только к следующему / предыдущему элементу. Большое спасибо за это, razzledazzle
AA_PV
LinearSnapHelper был первоначальным решением, которое сработало для меня, но похоже, что PagerSnapHelper улучшил анимацию прокрутки.
Леонардо
76

Обновление Google I / O 2019

ViewPager2 уже здесь!

Google только что объявил на своем выступлении «Что нового в Android» (также известное как «Тема Android»), что они работают над новым ViewPager на основе RecyclerView!

Со слайдов:

Как ViewPager, но лучше

  • Легкая миграция из ViewPager
  • На основе RecyclerView
  • Поддержка режима справа налево
  • Разрешает вертикальное перелистывание
  • Улучшенные уведомления об изменении набора данных

Вы можете проверить последнюю версию здесь и примечания к выпуску здесь . Также есть официальный образец .

Личное мнение: думаю, это действительно нужное дополнение. У меня недавно было много проблем с PagerSnapHelper бесконечно колеблющимся левым и правым движением - посмотрите билет, который я открыл.


Новый ответ (2016)

Теперь вы можете просто использовать SnapHelper .

Если вы хотите, чтобы поведение привязки по центру было похоже на ViewPager, используйте PagerSnapHelper :

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

Также существует LinearSnapHelper . Я пробовал, и если вы бросаете с энергией, то он прокручивает 2 элемента за 1 бросок. Лично мне это не понравилось, но решайте сами - проба занимает секунды.


Оригинальный ответ (2016)

После многих часов пробных 3 различных решений, найденных здесь, в SO, я наконец создал решение, которое очень точно имитирует поведение, обнаруженное в файле ViewPager.

Решение основано на решении @eDizzle , которое, как мне кажется, я улучшил достаточно, чтобы сказать, что оно работает почти как ViewPager.

Важно: RecyclerViewширина моих предметов точно такая же, как у экрана. С другими размерами не пробовал. Также использую с горизонталью LinearLayoutManager. Я думаю, что вам нужно будет адаптировать код, если вы хотите вертикальную прокрутку.

Вот код:

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

}

Наслаждайтесь!

Альберт Вила Кальво
источник
Приятно поймать onScrollStateChanged (); в противном случае, если мы медленно проведем по RecyclerView, появится небольшая ошибка. Благодарность!
Маурисио Сартори
Для поддержки полей (android: layout_marginStart = "16dp") я попытался использовать int screenwidth = getWidth (); и вроде работает.
eigil
AWESOOOMMEEEEEEEEE
радитья гумай 08
1
@galaxigirl нет, я еще не пробовал LinearSnapHelper. Я только что обновил ответ, чтобы люди знали об официальном решении. Мое решение работает без проблем в моем приложении.
Альберт Вила Кальво,
1
@AlbertVilaCalvo Я пробовал LinearSnapHelper. LinearSnapHelper работает со скроллером. Он прокручивается и щелкает в зависимости от силы тяжести и скорости броска. Чем выше скорость, тем больше страниц прокручивается. Я бы не рекомендовал его для поведения, подобного ViewPager.
mipreamble
46

Если цель состоит в том, чтобы RecyclerViewимитировать поведение, ViewPagerесть довольно простой подход.

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

Используя, PagerSnapHelperвы можете получить такое поведение, какViewPager

Бунья Китпитак
источник
4
Я использую LinearSnapHelperвместо, PagerSnapHelper и у меня это работает
fanjavaid
1
Да, здесь нет мгновенного эффектаLinearSnapHelper
Бунья Китпитак
1
это закрепляет элемент в представлении, дает возможность прокрутки
Сэм
@BoonyaKitpitak, я пробовал LinearSnapHelper, и он переключается на средний элемент. PagerSnapHelperмешает легко прокручивать (по крайней мере, список изображений).
CoolMind
@BoonyaKitpitak, вы можете сказать мне, как это сделать, один элемент в центре полностью видим, а боковые элементы видны наполовину?
Rucha Bhatt Joshi
30

Вам нужно использовать findFirstVisibleItemPosition для движения в противоположном направлении. А чтобы определить, в каком направлении было движение, вам нужно получить либо скорость броска, либо изменение x. Я подошел к этой проблеме под несколько иным углом, чем вы.

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

@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
источник
Где определяется screenWidth?
ltsai
@Itsai: Ой! Хороший вопрос. Это переменная, которую я установил в другом месте класса. Это ширина экрана устройства в пикселях, а не плотность в пикселях, поскольку для метода smoothScrollBy требуется количество пикселей, по которому он должен прокручиваться.
eDizzle 09
3
@ltsai Вы можете изменить на getWidth () (recyclerview.getWidth ()) вместо screenWidth. screenWidth работает некорректно, если ширина RecyclerView меньше ширины экрана.
VAdaihiep
Я попытался расширить RecyclerView, но получаю сообщение «Ошибка при раздувании пользовательского класса RecyclerView». Вы можете помочь? Спасибо
@bEtTyBarnes: вы можете поискать это в другом ленте вопросов. Это выходит за рамки первоначального вопроса.
eDizzle
6

Просто добавьте paddingи marginк recyclerViewи recyclerView item:

Элемент recyclerView:

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

recyclerView:

<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" />

и установите PagerSnapHelper:

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

dp в px:

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

результат:

введите описание изображения здесь

Фарзад
источник
Есть ли способ добиться переменного заполнения между элементами? например, если первую и последнюю карту не нужно центрировать, чтобы показать больше следующих / предыдущих карт по сравнению с центральными картами?
RamPrasadBismil
2

Мое решение:

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

Я передаю этот менеджер компоновки в RecycledView и устанавливаю смещение, необходимое для центрирования элементов. Все мои предметы имеют одинаковую ширину, поэтому постоянное смещение в порядке

анагаф
источник
0

PagerSnapHelperне работает с GridLayoutManagerspanCount> 1, поэтому мое решение в этом случае:

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
источник