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 дав swiping кращу анімацію.
Леонардо

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

AWESOOOMMEEEEEEEE
raditya gumay

1
@galaxigirl ні, я ще не пробував LinearSnapHelper. Я просто оновив відповідь, щоб люди знали про офіційне рішення. Моє рішення працює без будь-яких проблем у моєму додатку.
Альберт Віла Кальво,

1
@AlbertVilaCalvo Я спробував LinearSnapHelper. LinearSnapHelper працює зі скролером. Він прокручується та замикається залежно від сили тяжіння та швидкості польоту. Чим вище швидкість, тим більше сторінок прокручується. Я б не рекомендував його для поведінки, подібної до ViewPager.
міпреамбл

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, чи можете ви сказати мені, як це зробити, один елемент у центрі повністю видно, а бічні елементи видно наполовину?
Руча Бхатт Джоші

30

Вам потрібно використовувати findFirstVisibleItemPosition для руху в зворотному напрямку. А щоб визначити, в якому напрямку рухався рух, потрібно отримати або швидкість руху, або зміну х. Я підійшов до цієї проблеми з трохи іншого боку, ніж у вас.

Створіть новий клас, який розширює клас RecyclerView, а потім перевизначає метод перекидання 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;
}

Де визначається ширина екрану?
ltsai

@Itsai: На жаль! Хороше питання. Це змінна, яку я встановив деінде в класі. Це ширина екрана пристрою в пікселях, а не пікселі щільності, оскільки метод smoothScrollBy вимагає кількості пікселів, за якою потрібно прокрутити.
eDizzle

3
@ltsai Ви можете змінити на getWidth () (recyclerview.getWidth ()) замість screenWidth. screenWidth працює некоректно, коли RecyclerView менше ширини екрана.
VAdaihiep

Я намагався розширити RecyclerView, але я отримую "Помилка роздування класу користувацького RecyclerView". Ви можете допомогти? Дякую

@bEtTyBarnes: Можливо, ви захочете знайти це в іншому каналі запитань. Це досить поза рамками з початковим питанням тут.
eDizzle

6

Просто додайте paddingта marginдо recyclerViewта recyclerView item:

Переглянути елемент:

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

Перегляд:

<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
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.