SnapHelper, Expanding RecyclerView Function

Posted by chaddsuk on Thu, 16 May 2019 22:40:48 +0200

Preface

SnapHelper is a 24.2.0 version of support v4 package released by Google.
SnapHelper is an extension of RecyclerView functionality, making RecyclerView slide like ViewPager, no matter how slip ends up in the middle of a page.
ViewPager can only slide one page at a time, RecyclerView+SnapHelper can slide several pages at a time, and ultimately stay in the middle of a page. Very practical and cool.
SnapHelper is implemented by listening to the onFling interface in RecyclerView.OnFlingListener. LinearSnapHelper is a concrete implementation of the abstract class SnapHelper.

Realization effect

1.LinearSnapHelper is a self-contained implementation effect

Similar to ViewPager, it is easy to display a page in the middle, as long as the following two lines of code:

 LinearSnapHelper mLinearSnapHelper = new LinearSnapHelper();
 mLinearSnapHelper.attachToRecyclerView(recycleView);

Let's see how Linear SnapHelper implements SnapHelper. There are three main implementations:

1.calculateDistanceToFinalSnap()

    @Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

When the drag or slide ends, the method is called back, returning an out = int[2], out[0]x axis, out[1] y axis, which is the offset of the position you need to correct.

2.findSnapView()

    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

As you can see from the method name, finding the aligned view is the targetView of the previous method.

3.findTargetSnapPosition()

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return RecyclerView.NO_POSITION;
        }

        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        final View currentView = findSnapView(layoutManager);
        if (currentView == null) {
            return RecyclerView.NO_POSITION;
        }

        final int currentPosition = layoutManager.getPosition(currentView);
        if (currentPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }

        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        // deltaJumps sign comes from the velocity which may not match the order of children in
        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
        // get the direction.
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd == null) {
            // cannot get a vector for the given position.
            return RecyclerView.NO_POSITION;
        }

        int vDeltaJump, hDeltaJump;
        if (layoutManager.canScrollHorizontally()) {
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
            if (vectorForEnd.x < 0) {
                hDeltaJump = -hDeltaJump;
            }
        } else {
            hDeltaJump = 0;
        }
        if (layoutManager.canScrollVertically()) {
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY);
            if (vectorForEnd.y < 0) {
                vDeltaJump = -vDeltaJump;
            }
        } else {
            vDeltaJump = 0;
        }

        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
        if (deltaJump == 0) {
            return RecyclerView.NO_POSITION;
        }

        int targetPos = currentPosition + deltaJump;
        if (targetPos < 0) {
            targetPos = 0;
        }
        if (targetPos >= itemCount) {
            targetPos = itemCount - 1;
        }
        return targetPos;
    }

At the end of the slide, for OnFling, return the target alignment item position.

2. Customizing SnapHelper to Realize Left Alignment or Right Alignment

In fact, through the above analysis, you will find that the most important are the calculateDistance ToFinalSnap and findSnapView functions.

When looking for the target view, it's not as simple as Find Center View.
I think we need to consider the boundary condition of the final item. Bad judgment will occur, no matter how slippery the last item will not be able to fully display the bug.

package com.example.myapplication.com.example;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSnapHelper;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.view.View;


public class MySnapHelper extends LinearSnapHelper {

    // Left alignment
    public static final int TYPE_SNAP_START = 2;

    // Right alignment
    public static final int TYPE_SNAP_END = 3;

    // default
    private int type = TYPE_SNAP_START;

    @Nullable
    private OrientationHelper mVerticalHelper;
    @Nullable
    private OrientationHelper mHorizontalHelper;

    public MySnapHelper(int type) {
        this.type = type;
    }

    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        if (type == TYPE_SNAP_START) {
            return calculateDisOnStart(layoutManager, targetView);
        } else if (type == TYPE_SNAP_END) {
            return calculateDisOnEnd(layoutManager, targetView);
        } else {
            return super.calculateDistanceToFinalSnap(layoutManager, targetView);
        }
    }

    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (type == TYPE_SNAP_START) {
            return findStartSnapView(layoutManager);
        } else if (type == TYPE_SNAP_END) {
            return findEndSnapView(layoutManager);
        } else {
            return super.findSnapView(layoutManager);
        }
    }

    /**
     * TYPE_SNAP_START
     *
     * @param layoutManager
     * @param targetView
     * @return
     */
    private int[] calculateDisOnStart(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToStart(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToStart(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }


    /**
     * TYPE_SNAP_END
     *
     * @param layoutManager
     * @param targetView
     * @return
     */
    private int[] calculateDisOnEnd(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToEnd(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToEnd(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

    /**
     * calculate distance to start
     *
     * @param layoutManager
     * @param targetView
     * @param helper
     * @return
     */
    private int distanceToStart(@NonNull RecyclerView.LayoutManager layoutManager,
                                @NonNull View targetView, OrientationHelper helper) {
        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
    }


    /**
     * calculate distance to end
     *
     * @param layoutManager
     * @param targetView
     * @param helper
     * @return
     */
    private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
                              @NonNull View targetView, OrientationHelper helper) {
        return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
    }

    /**
     * find the start view
     *
     * @param layoutManager
     * @return
     */
    private View findStartSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findStartView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findStartView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }


    /**
     * Attention should be paid to judging the last item by judging the position on the right side of the distance.
     *
     * @param layoutManager
     * @param helper
     * @return
     */
    private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
            return null;
        }
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        final int start = helper.getStartAfterPadding();

        int absClosest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedStart(child);
            int absDistance = Math.abs(childStart - start);

            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }

        View firstVisibleChild = layoutManager.getChildAt(0);

        if (firstVisibleChild != closestChild) {
            return closestChild;
        }

        int firstChildStart = helper.getDecoratedStart(firstVisibleChild);

        int lastChildPos = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
        View lastChild = layoutManager.getChildAt(childCount - 1);
        int lastChildCenter = helper.getDecoratedStart(lastChild) + (helper.getDecoratedMeasurement(lastChild) / 2);
        boolean isEndItem = lastChildPos == layoutManager.getItemCount() - 1;
        if (isEndItem && firstChildStart < 0 && lastChildCenter < helper.getEnd()) {
            return lastChild;
        }

        return closestChild;
    }

    /**
     * find the end view
     *
     * @param layoutManager
     * @return
     */
    private View findEndSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findEndView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findEndView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

    private View findEndView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
            return null;
        }
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == 0) {
            return null;
        }

        View closestChild = null;
        final int end = helper.getEndAfterPadding();

        int absClosest = Integer.MAX_VALUE;
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedEnd(child);
            int absDistance = Math.abs(childStart - end);

            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }

        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);

        if (lastVisibleChild != closestChild) {
            return closestChild;
        }

        if (layoutManager.getPosition(closestChild) == ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()) {
            return closestChild;
        }

        View firstChild = layoutManager.getChildAt(0);
        int firstChildStart = helper.getDecoratedStart(firstChild);

        int firstChildPos = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        boolean isFirstItem = firstChildPos == 0;


        int firstChildCenter = helper.getDecoratedStart(firstChild) + (helper.getDecoratedMeasurement(firstChild) / 2);
        if (isFirstItem && firstChildStart < 0 && firstChildCenter > helper.getStartAfterPadding()) {
            return firstChild;
        }

        return closestChild;
    }


    @NonNull
    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
        if (mVerticalHelper == null) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
        }
        return mVerticalHelper;
    }

    @NonNull
    private OrientationHelper getHorizontalHelper(
            @NonNull RecyclerView.LayoutManager layoutManager) {
        if (mHorizontalHelper == null) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return mHorizontalHelper;
    }
}

Finally, just use our own SnapHelper, and you can do it easily.

MySnapHelper mySnapHelper = new MySnapHelper(2);
mySnapHelper.attachToRecyclerView(recycleView);

ps:

If the separator is used in the above code, the displacement will be erroneous when aligning in the center and right.
The reason is that the targetView contains item s and separators when calculating offsets. So we need to subtract the width of the divider when calculating the offset.
Take right alignment as an example: in distanceToEnd ()

private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
                              @NonNull View targetView, OrientationHelper helper) {
        //No dividing line
        return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();

    }

Change to

private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
                              @NonNull View targetView, OrientationHelper helper) {
        //Split line
        return helper.getDecoratedStart(targetView) - helper.getEndAfterPadding() + targetView.getWidth();
    }

If the alignment + separator is used, because the linearSnapHelper can not be changed, we can create a new class to inherit SnapHelper, copy all the code in LinearSnapHelper, just change the distanceToCenter () method.

Okay, that's basically all right.
Finally, I suggest that the best way to use this effect is not to use separators...

Topics: Android Google