[android] RecyclerView 가로 스크롤 스냅

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

오른쪽에서 왼쪽으로 스 와이프는 이제 잘 작동하지만 그 반대는 아닙니다. 여기서 무엇을 놓치고 있습니까?



답변

를 사용하면 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"


답변

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

}

즐겨!


답변

목표가 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


답변

반대 방향으로 가려면 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;
}


답변

그냥 추가 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());
}

결과:

여기에 이미지 설명 입력


답변

내 솔루션 :

/**
 * 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에 전달하고 항목을 가운데에 맞추는 데 필요한 오프셋을 설정합니다. 내 모든 항목의 너비가 같으므로 일정한 오프셋이 괜찮습니다.


답변

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