넷플릭스 같은 RecyclearView [Snap Helper]

Snapping이란?


안드로이드에서는 SnapHelper 레벨을 사용하여 RecyclearView의 Snapping을 실현할 수 있습니다.
스냅샷?스냅숏이란 굴러갈 때 보통 그렇게 플링하는 것이 아니라 특정한 물품으로 신속하게 멈추는 행위라고 생각하는 사람이 있을 것 같다.
넷플릭스의 메인 화면에는 스냅핑이 사용됐다.

Snapping 구현 방법


이번에는 안드로이드의 스냅핑을 구현하는 방법에 대해 기술한다.

RecyclearView 스크롤 State


RecyclearView 스크롤에는 세 가지 상태가 있습니다.
  • SCROLL_STATE_IDLE(RecyclearView가 스크롤되지 않은 상태)
  • SCROLL_STATE_DRAGGING(RecyclearView가 드래그된 상태)
  • SCROLL_STATE_SETTLING(RecyclearView 애니메이션의 최종 위치 상태)
  • SnapHelper


    이제 이 주제의 SnapHelper 내부를 살펴보겠습니다.
    SnapHelper에는 3가지 Abstract 방법이 있습니다.
    여기서 Override가 의도한 Snaping을 구현할 수 있습니다.

    calculateDistanceToFinalSnap


    cal culateDistanceToFinal Snap은 Snap을 원하는 View(targetView)에 따라 최종 위치로 반환됩니다.
    /**
    * Override this method to snap to a particular point within the target view or the container
    * view on any axis.
    * <p>
    * This method is called when the {@link SnapHelper} has intercepted a fling and it needs
    * to know the exact distance required to scroll by in order to snap to the target view.
    *
    * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
    *                      {@link RecyclerView}
    * @param targetView the target view that is chosen as the view to snap
    *
    * @return the output coordinates the put the result into. out[0] is the distance
    * on horizontal axis and out[1] is the distance on vertical axis.
    */
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
    
    targetView 빨리 찍고 싶은 View.
    위의 넷플릭스와 같은 행동을 실현하기 위해 targetView를 중심으로 스냅 처리를 했고, targetView의 중심과 RecyclearView 자체 중심 위치의 차이만 돌려주면 된다.
    
    @Override
    int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = getDistance(layoutManager, targetView, OrientationHelper.createHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
    
        if (layoutManager.canScrollVertically()) {
            out[1] = getDistance(layoutManager, targetView, OrientationHelper.createVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }
    
    int getDistance(RecyclerView.LayoutManager layoutManager, View targetView, OrientationHelper helper) {
        final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter = layoutManager.getClipToPadding()
                ? helper.getStartAfterPadding() + helper.getTotalSpace() / 2
                : helper.getEnd() / 2;
        return childCenter - containerCenter;
    }
    
    

    findSnapView


    findSnapview에서 Snap을 원하는 View를 이름별로 반환합니다.여기서 반환된 View는 위의 cal culateDistanceToFinalsnap 매개 변수로 들어갑니다.
    이 방법은 스크롤 상태SCROLL_STATE_IDLE와 SnapHelper가 RecyclearView에서 Attach라고 부른다.
    
    /**
    * Override this method to provide a particular target view for snapping.
    * <p>
    * This method is called when the {@link SnapHelper} is ready to start snapping and requires
    * a target view to snap to. It will be explicitly called when the scroll state becomes idle
    * after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap
    * after a fling and requires a reference view from the current set of child views.
    * <p>
     * If this method returns {@code null}, SnapHelper will not snap to any view.
    *
    * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
    *                      {@link RecyclerView}
    *
    * @return the target view to which to snap on fling or end of scroll
    */
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract View findSnapView(LayoutManager layoutManager);
    
    
    위의 넷플릭스와 같은 행동을 실현하기 위해 3개의 item이 함께 굴러가기 때문에 item의position은 1, 4, 7...그리고 센터에 가장 가까운view를 돌려주시면 됩니다.
    
    @Override
    View findSnapView(RecyclerView.LayoutManager layoutManager) {
        OrientationHelper helper = layoutManager.canScrollHorizontally()
                ? OrientationHelper.createHorizontalHelper(layoutManager)
                : OrientationHelper.createVerticalHelper(layoutManager);
        int childCount = layoutManager.getChildCount();
        View closestChild = null;
        int containerCenter = layoutManager.getClipToPadding()
                ? helper.getStartAfterPadding() + helper.getTotalSpace() / 2
                : helper.getEnd() / 2;
        int absClosest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            if (child == null) continue;
            if (getChildPosition(child, helper) % 3 != 1) continue;
            int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2);
            int absDistance = Math.abs(childCenter - containerCenter);
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }
    
    

    findTargetSnapPosition


    findTargetSnapPosition에서 targetView의 Position을 반환합니다.언뜻 보기에는findSnapview와 별 차이가 없지만 방법의 호출 시기는 다르다.
    findTarget SnapPosition은 RecyclearView가 SCROLL_STATE_SETTLING로 변한 상태에서 불린다.따라서 View 자체는 생성되지 않을 수 있으므로 View가 아닌 View의 Position입니다.
    /**
     * Override to provide a particular adapter target position for snapping.
     *
     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}
     * @param velocityX fling velocity on the horizontal axis
     * @param velocityY fling velocity on the vertical axis
     *
     * @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
     *         if no snapping should happen
     */
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY);
    
    
    넷플릭스와 같은 동작을 수행하려면 스크롤 방향에 따라 잡은 View의 Positionw를 반환합니다.
    
    @Override
    int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        boolean forwardDirection = layoutManager.canScrollHorizontally() ? velocityX > 0 : velocityY > 0;
        return forwardDirection ? previousClosestPosition + 3 : previousClosestPosition - 3;
    }
    
    
    previousCloset Position에는 findSnapview가 없으며 마지막 Snap의 Position을 구성원 변수 등에 저장하면 됩니다.

    최후


    SnapHelper의 사용법을 상세히 썼지만, 이번에 쓴 내용을 대량으로 사용해 프로그램 라이브러리를 만들었다!
    Snap 방향Gravity과 스크롤할 item 수SnapCount를 지정할 수 있습니다.
    꼭 보셔야 한다면 정말 감사합니다.
    그리고 괜찮은 사람이 꼭 스타가 된다면 고마워요!
    feature1: Gravity
    feature2: SnapCount

    좋은 웹페이지 즐겨찾기