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가 더 나은 애니메이션을 제공 한 것 같습니다.
LeonardoSibela

76

Google I / O 2019 업데이트

ViewPager2 가 여기 있습니다!

Google은 방금 'Android의 새로운 기능'(일명 'Android 키 노트')에서 RecyclerView를 기반으로하는 새로운 ViewPager를 개발 중이라고 발표했습니다!

슬라이드에서 :

ViewPager와 비슷하지만 더 좋습니다.

  • ViewPager에서 쉽게 마이그레이션
  • RecyclerView 기반
  • 오른쪽에서 왼쪽으로 모드 지원
  • 수직 페이징 허용
  • 향상된 데이터 세트 변경 알림

당신은 최신 버전을 확인할 수 있습니다 여기에 와 릴리스 노트 여기 . 도 있습니다 공식 샘플 .

개인적 의견 : 이것이 정말로 필요한 추가라고 생각합니다. 나는 최근에 PagerSnapHelper 왼쪽 오른쪽이 무한정 진동 하는 데 많은 문제를 겪었 습니다. 내가 개봉 한 티켓을 보십시오 .


새로운 답변 (2016)

이제 SnapHelper를 사용할 수 있습니다 .

ViewPager 와 유사한 가운데 정렬 된 스냅 동작을 원하면 PagerSnapHelper 를 사용 하십시오 .

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

도있다 LinearSnapHelper은 . 나는 그것을 시도하고 당신이 에너지로 날뛰면 그것은 1 날 뛰기로 2 항목을 스크롤합니다. 개인적으로 나는 그것을 좋아하지 않았지만 스스로 결정하십시오.


원문 답변 (2016)

여기에서 찾은 3 가지 다른 솔루션을 여러 시간 시도한 후 마침내 .NET Framework에서 발견 된 동작을 매우 가깝게 모방하는 솔루션을 구축했습니다 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를 천천히 스 와이프하면 작은 버그가 있습니다. 감사!
Mauricio Sartori

여백을 지원하기 위해 (android : layout_marginStart = "16dp") int screenwidth = getWidth (); 작동하는 것 같습니다.
eigil

AWESOOOMMEEEEEEEEE
gumay raditya

1
@galaxigirl 아니, 아직 LinearSnapHelper를 시도하지 않았습니다. 사람들이 공식 솔루션에 대해 알 수 있도록 답변을 업데이트했습니다. 내 솔루션은 내 앱에서 문제없이 작동합니다.
Albert Vila Calvo

1
@AlbertVilaCalvo 나는 LinearSnapHelper를 시도했습니다. LinearSnapHelper는 Scroller와 함께 작동합니다. 중력과 날뛰는 속도에 따라 스크롤하고 스냅합니다. 속도가 빠를수록 더 많은 페이지가 스크롤됩니다. 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
Boonya Kitpitak은

1
이것은보기에 항목을 고정하여 스크롤 할 수있게합니다
Sam

@BoonyaKitpitak, 시도했는데 LinearSnapHelper중간 항목에 스냅됩니다. PagerSnapHelper(적어도 이미지 목록) 쉽게 스크롤 할 수 없습니다.
CoolMind

@BoonyaKitpitak이 작업을 수행하는 방법을 알려 주시겠습니까? 하나의 항목은 완전히 표시되고 측면 항목은 절반이 표시됩니까?
Rucha Bhatt Joshi

30

반대 방향으로 가려면 findFirstVisibleItemPosition을 사용해야합니다. 그리고 스 와이프가 어느 방향에 있었는지 감지하려면 플링 속도 또는 x의 변화를 가져와야합니다. 나는 당신이 가지고있는 것과 약간 다른 각도에서이 문제에 접근했습니다.

RecyclerView 클래스를 확장하는 새 클래스를 만든 다음 RecyclerView의 fling 메서드를 다음과 같이 재정의합니다.

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

screenWidth는 어디에 정의되어 있습니까?
ltsai

@Itsai : 죄송합니다! 좋은 질문. 클래스의 다른 곳에서 설정 한 변수입니다. smoothScrollBy 메서드는 스크롤 할 픽셀 수를 요구하기 때문에 밀도 픽셀이 아닌 픽셀 단위의 장치 화면 너비입니다.
eDizzle 2015

3
@ltsai screenWidth 대신 getWidth () (recyclerview.getWidth ())로 변경할 수 있습니다. RecyclerView가 화면 너비보다 작을 때 screenWidth가 제대로 작동하지 않습니다.
VAdaihiep

RecyclerView를 확장하려고했지만 "Error inflating class custom RecyclerView"가 나타납니다. 도울 수 있니? 감사합니다

@bEtTyBarnes : 다른 질문 피드에서 검색 할 수 있습니다. 여기의 초기 질문으로는 범위를 벗어났습니다.
eDizzle

6

그냥 추가 paddingmarginrecyclerViewrecyclerView 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

PagerSnapHelperGridLayoutManagerspanCount> 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
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.