Program Club

RecyclerView의 빠른 스크롤

proclub 2020. 11. 6. 20:55
반응형

RecyclerView의 빠른 스크롤


RecyclerView스크롤 할 때 구성 요소가 특정 요소에 스냅되도록하는 시나리오에 대해 클래스 를 사용하려고합니다 ( Gallery가운데 잠금 항목이있는 목록의 예로 이전 Android 가 떠 오릅니다).

이것이 내가 지금까지 취하는 접근 방식입니다.

나는 초기 플링 속도가 주어 졌을 때 뷰가 스크롤을 종료해야하는 위치를 계산 ISnappyLayoutManager하는 메서드를 포함 하는 인터페이스를 가지고 있습니다 getPositionForVelocity.

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

그런 다음 보기에 정확한 양을 표시하는 방식으로 fling () 메서드 SnappyRecyclerView를 하위 클래스로 지정 RecyclerView하고 재정의 하는 클래스 가 있습니다.

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'의 철학에 반하는 것 같습니다. 둘째, default를 사용하고 싶다면 LinearLayoutManager현재 스크롤 상태를 이해하고 스크롤 위치를 정확히 계산하기 위해 내부를 엉망으로 만들어야하기 때문에 다소 복잡해집니다. 마지막으로 목록을 이동 한 다음 일시 중지했다가 손가락을 떼면 플링 이벤트가 발생하지 않고 (속도가 너무 낮음) 목록이 중간에 남아있는 것처럼 가능한 모든 스크롤 시나리오를 처리하지 않습니다. 위치. 이것은 스크롤 상태 리스너를에 추가하여 처리 할 수 RecyclerView있지만 매우 엉망인 것 같습니다.

뭔가 빠진 것 같아. 이 작업을 수행하는 더 좋은 방법이 있습니까?


를 사용하면 LinearSnapHelper이제 매우 쉽게 할 수 있습니다.

다음과 같이하면됩니다.

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

간단합니다! 참고 LinearSnapHelper지원 라이브러리 버전 24.2.0부터 추가되었습니다.

이는 앱 모듈의 build.gradle

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

나는 위와 약간 다른 것을 내놓았다. 이상적이지는 않지만 나를 위해 잘 작동하고 다른 사람에게 도움이 될 수 있습니다. 나는 다른 누군가가 더 좋고 덜 험난한 것을 함께 제공하기를 바라 면서이 대답을 받아들이지 않을 것입니다 (그리고 RecyclerView구현을 오해 하고 이것을하는 간단한 방법을 놓칠 가능성이 있지만 그동안 이것으로 충분합니다. 정부 업무를 위해!)

구현의 기본은 다음과 같다 : a의 스크롤 RecyclerView사이 분할 최대의 일종이다 RecyclerViewLinearLayoutManager. 처리해야 할 두 가지 경우가 있습니다.

  1. 사용자가보기를 튕 깁니다. 기본 동작은 RecyclerView플링을 내부로 전달한 Scroller다음 스크롤링 마법을 수행하는 것입니다. 이는 RecyclerView일반적으로 스냅되지 않은 위치에 정착 하기 때문에 문제가 됩니다. RecyclerView 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오는를 하위 클래스 만드는 예제입니다 .LinearLayoutManagerLayoutManager

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, i.e. 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;
    }

}

나는 이것을 할 더 깨끗한 방법을 찾았습니다. @Catherine (OP)이 개선 될 수 있는지 또는 귀하의 것보다 개선되었다고 생각하는지 알려주십시오. :)

여기 내가 사용하는 스크롤 리스너가 있습니다.

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

예를 들어 여기에서 몇 가지 사소한 가정을 생략했습니다.

1) 초기 및 최종 패딩 제 1 및 수평 스크롤을 필요로하는 마지막 항목은에 첫 번째와 마지막 respectively.For 예를 스크롤 할 때 초기 및 최종 견해 중심에 있음을 각각도록 설정 초기 및 최종 패딩해야합니다 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);
    }
}

논리는 매우 일반적이며 다른 많은 경우에 사용할 수 있습니다. 제 경우에는 리사이클 러 뷰가 수평이고 여백 (기본적으로 recyclerview의 중심 X 좌표는 화면의 중심) 또는 고르지 않은 패딩없이 전체 가로 너비를 늘립니다.

누군가가 문제에 직면하고 있다면 친절하게 의견을 말하십시오.


나는 또한 멋진 리사이클 러 뷰가 필요했습니다. 리사이클 러 뷰 항목이 열 왼쪽에 스냅되도록하고 싶습니다. 리사이클 러 뷰에서 설정 한 SnapScrollListener를 구현하는 것으로 끝났습니다. 이것은 내 코드입니다.

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

smoothScrollToPosition (int position)에 대한 호출로 fling 메서드를 재정의합니다. 여기서 "int position"은 어댑터에서 원하는 뷰의 위치입니다. 당신은 어떻게 든 위치의 가치를 얻어야 할 것이지만 그것은 당신의 필요와 구현에 달려 있습니다.


RecyclerView를 조금 엉망으로 만든 후, 이것이 지금까지 생각해 낸 것이고 지금 사용하고있는 것입니다. 하나의 사소한 결함이 있지만 아마 눈치 채지 못할 것이므로 (아직) 콩을 흘리지 않을 것입니다.

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

수평 recyclerview 만 지원하고 중앙에 스냅하며 중앙에서 얼마나 멀리 떨어져 있는지에 따라 뷰를 축소 할 수도 있습니다. RecyclerView의 대체물로 사용하세요.

편집 : 08/2016 저장소로 만들었습니다 :
https://github.com/lauw/Android-SnappingRecyclerView
더 나은 구현을 위해 작업하는 동안이 상태를 유지하겠습니다.


스냅-투-포지션 동작을 달성하기위한 매우 간단한 접근 방식-

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

        }
    });

유일한 작은 단점은 부분적으로 보이는 셀의 절반 이하로 스크롤 할 때 뒤로 스냅되지 않는다는 것입니다. 그러나 이것이 당신을 괴롭히지 않으면 깨끗하고 간단한 솔루션입니다.


Snap Scroll은 이제 지원 라이브러리 24.2.0의 일부입니다.

https://developer.android.com/topic/libraries/support-library/revisions.html

관련 클래스 :

https://developer.android.com/reference/android/support/v7/widget/SnapHelper.html

https://developer.android.com/reference/android/support/v7/widget/LinearSnapHelper.html

다음은 좋은 예입니다.

https://rubensousa.github.io/2016/08/recyclerviewsnap


시작, 상단, 종료 또는 하단에 스냅 지원이 필요한 경우 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);

GravitySnapHelper로 상단 스냅 :

topRecyclerView.setLayoutManager(new LinearLayoutManager(this));

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

저는 RecyclerView의 수평 방향에 대한 작업 솔루션을 구현했습니다. 이는 첫 번째 MOVE 및 UP에서 좌표 onTouchEvent를 읽는 것입니다. 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;
}}

나를 위해 잘 작동합니다. 문제가 있으면 알려주십시오.


겸손한 사람의 답변을 업데이트하려면 :

이 스크롤 리스너는 실제로 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);
            }
        }
    });

또 다른 깨끗한 옵션은 custom을 사용하는 것입니다. https://github.com/apptik/multiview/tree/master/layoutmanagersLayoutManager 를 확인할 수 있습니다.

개발 중이지만 꽤 잘 작동합니다. 스냅 샷 사용 가능 : 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);

    }
}

참고 URL : https://stackoverflow.com/questions/26370289/snappy-scrolling-in-recyclerview

반응형