Android: Let you use Recycler View in a clear way - SnapHelper details

brief introduction

SnapHelper is added to RecyclerView in version 24.2.0 to assist RecyclerView to align Item to a certain location at the end of scrolling. Especially when the list slides horizontally, it will not slide to any position in many cases, but there will be some rules restriction. At this time, the alignment rules can be defined by SnapHelper.

SnapHelper is an abstract class. Officially, it provides a subclass of LinearSnapHelper that allows the corresponding Item to stay in the middle when RecyclerView scrolls to stop. The official version of 25.1.0 also provides a subclass of PagerSnapHelper, which makes RecyclerView look like ViewPager. It can only slide one page at a time and display in the middle.

These two subclasses are also easy to use, just call attachToRecyclerView() after creating the object and attach it to the corresponding RecyclerView object.

new LinearSnapHelper().attachToRecyclerView(mRecyclerView);
//perhaps
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);

Principle analysis

Fling operation

First of all, to understand a concept, a finger slides on the screen Recycler View and then releases its hand. The content in Recycler View will continue to roll along the direction of inertia until it stops. This process is called Fling. The Fling operation is triggered as soon as the finger leaves the screen and ends when the scroll stops.

Three abstract methods

SnapHelper is an abstract class with three abstract methods:

public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY)

This method will find out where RecyclerView needs to scroll according to the rate of triggering Fling operation (parameter velocityX and parameter velocityY). The ItemView corresponding to that location is the list item that needs to be aligned. We call this location targetSnapPosition, and the corresponding View is called targetSnapView. If targetSnapPosition is not found, return RecyclerView.NO_POSITION.

public abstract View findSnapView(LayoutManager layoutManager)

This method finds the view closest to the alignment position on the current layoutManager, which is called SanpView and the corresponding position is called SnapPosition. If null is returned, it means that there is no View that needs to be aligned, and no scroll alignment adjustment will be made.

public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);

This method calculates the distance between the ItemView current coordinates corresponding to the second parameter and the coordinates that need to be aligned. This method returns an int array of size 2, which corresponds to the distances in the x-axis and y-axis directions, respectively.

attachToRecyclerView()

Now let's look at the attachToRecyclerView() method, through which SnapHelper attaches to RecyclerView to assist the RecyclerView scroll alignment operation. The source code is as follows:

   public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
      //If SnapHelper has previously been attached to this RecyclerView, no action is required.
        if (mRecyclerView == recyclerView) {
            return;
        }
      //If the RecyclerView attached to SnapHelper is inconsistent with the current one, clean up the callbacks from the former RecyclerView
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
      //Update RecyclerView object reference
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
          //Set the callback for the current RecyclerView object
            setupCallbacks();
          //Create a Scroller object to assist in calculating the total distance of fling, which will be covered later
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
          //Call the snapToTargetExistingView() method to achieve the scrolling of SnapView
            snapToTargetExistingView();
        }
    }

As you can see, the callback of the RecyclerView object saved before SnapHelper is cleared in the attachToRecyclerView() method (if any), the callback of the newly set RecyclerView object is set, a Scroller object is initialized, and the snapToTargetExistingView() method is called to align SnapView.

snapToTargetExistingView()

The function of this method is to adjust SnapView by scrolling so that SnapView can achieve alignment effect. The source code is as follows:

    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
      //Find SnapView
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
      //Calculate the distance SnapView needs to scroll
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
      //If the distance required to scroll is not zero, call smoothScrollBy () to scroll the RecyclerView to the appropriate distance
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }

As you can see, the snapToTargetExistingView() method finds SnapView first, calculates the distance between the current coordinate of SnapView and the destination coordinate, and then calls RecyclerView.smoothScrollBy() method to achieve smooth scrolling of the content of RecyclerView, thus moving SnapView to the target position to achieve alignment effect. The implementation principle of RecyclerView.smoothScrollBy() is not expanded here. Its function is to smooth the distance of ItemView in RecyclerView according to the parameters.

setupCallbacks() and destroyCallbacks()

Look at what callbacks SnapHelper has set for RecyclerView:

    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }

    private void destroyCallbacks() {
        mRecyclerView.removeOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(null);
    }

You can see that there are two callbacks set by RecyclerView: one is the OnScrollListener object mScrollListener, and the other is the OnFlingListener object. Because SnapHelper implements the OnFlingListener interface, this object is SnapHelper itself.

Let's first look at how the mScrollListener variable is implemented.

    private final RecyclerView.OnScrollListener mScrollListener =
            new RecyclerView.OnScrollListener() {
                boolean mScrolled = false;
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                  //mScrolled is true to indicate that scrolling has been done before.
                  //newState represents the end of the scroll stop for the SCROLL_STATE_IDLE state
                    if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                        mScrolled = false;
                        snapToTargetExistingView();
                    }
                }

                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (dx != 0 || dy != 0) {
                        mScrolled = true;
                    }
                }
            };

The implementation of the scroll listener is very simple, except that snapToTargetExistingView() method is called to scroll the targetView when the normal scroll stops to ensure that the stop position is on the corresponding coordinates, which is the purpose of adding the OnScrollListener to RecyclerView.

In addition to OnScrollListener, the RecyclerView also has OnFlingListener, which is SnapHelper itself. Because SnapHelper implements the RecyclerView.OnFlingListener interface. Let's first look at the RecyclerView.OnFlingListener interface.

    public static abstract class OnFlingListener {
            /**
         * Override this to handle a fling given the velocities in both x and y directions.
         * Note that this method will only be called if the associated {@link LayoutManager}
         * supports scrolling and the fling is not handled by nested scrolls first.
         *
         * @param velocityX the fling velocity on the X axis
         * @param velocityY the fling velocity on the Y axis
         *
         * @return true if the fling washandled, false otherwise.
         */
        public abstract boolean onFling(int velocityX, int velocityY);
    }

There is only one onFling() method in this interface, which is called when RecyclerView starts fling. Let's see how SnapHelper implements the onFling() method:

    @Override
    public boolean onFling(int velocityX, int velocityY) {
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
      //Get the minimum rate required for the RecyclerView fling operation.
      //Only beyond that rate will ItemView have enough power to continue scrolling as the fingers leave the screen.
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
      //The snapFromFling() method is called here to achieve smooth scrolling and align itemView to the destination coordinate when the scroll stops.
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }

The annotations are well explained. See how snapFromFling() works:

    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
      //Layout Manager must implement the ScrollVectorProvider interface to continue
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }

      //Create a SmoothScroller object, which is a smooth scroller for smooth scrolling of ItemView
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }

      //Target SnapPosition () is found by findTargetSnapPosition() method with layoutManager and rate as parameters.
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        //Set the rolling target position of the scroller by setTargetPosition() method
        smoothScroller.setTargetPosition(targetPosition);
        //Start the smooth scroller with layoutManager and start scrolling to the target position
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

As you can see, the snapFromFling() method first determines whether the layoutManager implements the ScrollVectorProvider interface, and if it does not, it does not allow scrolling through this method. So why do we have to implement this interface? I'll explain it later. Next, create an example of SmoothScroller, which layoutManager can use to roll. SmoothScroller needs to set a rolling target position. We will give it the target SnapPosition calculated by findTargetSnapPosition() method, tell the scroller to roll to this position, and then start SmoothScroller to roll.

But here's one thing to note. By default, SmoothScroller set by setTargetPosition() method can only scroll the ItemView of the corresponding location to the border alignment with RecyclerView. How can we scroll the ItemView to the target location we need to align? You have to deal with SmoothScroller.

Look at the RecyclerView.SmoothScroller, which is created by the createSnapScroller() method:

    @Nullable
    protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
      //Similarly, the first step is to determine whether the layoutManager implements the ScrollVectorProvider interface.
      //SmoothScroller will not be created without implementing the interface
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return null;
        }
      //Here we create a LinearSmoothScroller object and return it to the calling function.
      //That is to say, the line Smooth Scroller that was eventually created is the line Smooth Scroller.
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
          //This method is called when the targetSnapView is laid out.
          //This method has three parameters:
          //The first parameter, targetView, is the targetSnapView described in this article.
          //The second parameter, RecyclerView.State, is not used here, let alone it.
          //The third parameter, Action, what is this? It is a static inner class of SmoothScroller.
          //Keep some information about SmoothScroller's smooth rolling process, such as rolling time, rolling distance, differentiator, etc.
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
             //The calculateDistanceToFinalSnap () method is explained above.
             //Get the distance between the current coordinate and the destination coordinate of the targetSnapView
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                        targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
              //The calculateTimeForDeceleration () method is used to obtain the time required for decelerating rolling.
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                  //Call the update() method of Action to update the scroll rate of SmoothScroller so that it slows down and scrolls to stop.
                  //The effect here is that the SmoothScroller spends so much time rolling dx or dy for such a long distance at the rolling rate of the mDecelerate Interpolator differential.
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }

          //This method calculates the rolling rate, and the return value represents the rolling rate, which affects the value just mentioned above.
          //The return value of the calculateTimeForDeceleration () method,
          //MILLISECONDS_PER_INCH has a value of 100, which means that the return value of the method represents a scroll of 100 milliseconds per dpi distance.
            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }

From the above analysis, we can see that createSnapScroller() creates a LinearSmoothScroller, and mainly considers two aspects when creating the LinearSmoothScroller:

  • The first is the rolling rate, which is determined by the calculateSpeedPerPixel() method.

  • The second is that in the process of rolling, when the targetView is about to enter the field of vision, the uniform rolling is transformed into decelerating rolling, and then the target coordinate position is continuously rolled to make the rolling effect more real, which is determined by onTargetFound() method.

Didn't you just leave a question? In normal mode, the ItemView set by SmoothScroller through setTargetPosition() method can only scroll to the edge of RecyclerView alignment, and the solution to this limitation is in SmoothScroller's onTargetFound() method. The onTargetFound() method is called when the targetSnapView is laid out during the SmoothScroller scroll. At this time, calculateDistanceToFinalSnap() method is used to get the distance between the target SnapView's current coordinate and the target coordinate, and then Action.update() method is used to change the status of the current SmoothScroller, so that SmoothScroller can scroll according to the new rolling distance, new rolling time and new rolling differential. It can not only scroll targetSnapView to the target coordinate position, but also realize deceleration rolling, which makes the rolling effect more real.

As you can see from the figure, most of the time when targetSnapView is laid out (onTargetFound() method is called) is not next to Item on the interface, but ahead of time, because RecyclerView has a pre-loading process when it slides and scrolls in order to optimize performance and improve fluency. Item to layout, this knowledge point involves a lot of content, here do an understanding of it, do not expand in detail, there will be time to talk about Recycler View related principles and mechanisms.

Here's the idea: SnapHelper implements the OnFlingListener interface, in which the onFling() method is called when RecyclerView triggers the Fling operation. In the onFling() method, the speed in the current direction is judged to be sufficient for rolling operation. If the speed is large enough, the snapFromFling() method is called to implement rolling-related logic. In the snapFromFling() method, a SmoothScroller is created, and the position at which the scroll stops is calculated based on the rate, which is set to SmoothScroller and the scroll is started. SmoothScroller is responsible for all rolling operations. It can control the rolling speed of Item (initially uniform) and change the rolling speed (converted to deceleration) when rolling to targetSnapView is laid out to make the rolling effect more realistic.

So SnapHelper assists RecyclerView to achieve scroll alignment by setting OnScrollerListenerh and OnFlingListener to RecyclerView.

LinearSnapHelper

SnapHelper's framework for assisting RecyclerView scroll alignment has been set up, and the subclasses need only implement the three abstract methods according to alignment. Take Linear SnapHelper as an example to see how it realizes the three abstract methods of SnapHelper so that ItemView scrolls in the middle:

calculateDistanceToFinalSnap()

    @Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
      //When rolling horizontally, the distance needed to roll horizontally is calculated. Otherwise, the distance needed to roll horizontally is 0.
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

      //When rolling in vertical direction, the rolling distance in vertical direction is calculated, otherwise the rolling distance in horizontal direction is 0.
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

This method returns the distance between the view corresponding to the second parameter and the middle position of RecyclerView, and can support the calculation of horizontal and vertical rolling directions. The main method for calculating distance is distanceToCenter():

    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
      //Find the central coordinates of the targetView
        final int childCenter = helper.getDecoratedStart(targetView) +
                (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
      //Find the central coordinates of the container (Recycler View)
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
      //The difference between the two central coordinates is the distance the targetView needs to scroll.
        return childCenter - containerCenter;
    }

As you can see, the distance between the central coordinates of the corresponding view and the central coordinates of the RecyclerView is calculated, which is the distance that the view needs to scroll.

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

Look for SnapView, where the purpose coordinate is the middle position coordinate of RecyclerView. You can see that the calculation will be distinguished according to the layout of the layoutManager (horizontal layout or vertical layout), but ultimately the snapView is found through the findCenterView() method.

    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
      //Find the central coordinates of RecyclerView
        final int center;
        if (layoutManager.getClipToPadding()) {
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;

      //Traverse through all ItemView s in the current layoutManager
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
          //Central coordinates of ItemView
            int childCenter = helper.getDecoratedStart(child) +
                    (helper.getDecoratedMeasurement(child) / 2);
          //Calculate the distance between the ItemView and RecyclerView central coordinates
            int absDistance = Math.abs(childCenter - center);

            //Comparing the distance between each ItemView and the center of RecyclerView, find the ItemView closest to the center and return.
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }

The annotations are explained very clearly and will not be repeated.

findTargetSnapPosition()

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

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

      //Find snapView
        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;
        // Through the computeScrollVectorForPosition () method in the ScrollVectorProvider interface
        // Determine the layout direction of layoutManager
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd == null) {
            return RecyclerView.NO_POSITION;
        }

        int vDeltaJump, hDeltaJump;
        if (layoutManager.canScrollHorizontally()) {
          //Layout Manager is a horizontal layout, and the content goes beyond the screen. canScrollHorizontally() returns true
          //Estimate the lateral position offset at the end of fling relative to the current snapView position
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
          //Vector ForEnd. x < 0 means that the layoutManager is in reverse layout, so the offset is reversed.
            if (vectorForEnd.x < 0) {
                hDeltaJump = -hDeltaJump;
            }
        } else {
          //If you can't roll horizontally, the offset of horizontally position is of course zero.
            hDeltaJump = 0;
        }

      //Vertical Principle Ibid.
        if (layoutManager.canScrollVertically()) {
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY);
            if (vectorForEnd.y < 0) {
                vDeltaJump = -vDeltaJump;
            }
        } else {
            vDeltaJump = 0;
        }

      //According to the horizontal and vertical layout of layoutManager, the final horizontal offset and vertical offset are chosen as the fling offset.
        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
        if (deltaJump == 0) {
            return RecyclerView.NO_POSITION;
        }
        //The current position plus the offset position gives you the position at the end of the fling, which is the targetPosition.
        int targetPos = currentPosition + deltaJump;
        if (targetPos < 0) {
            targetPos = 0;
        }
        if (targetPos >= itemCount) {
            targetPos = itemCount - 1;
        }
        return targetPos;
    }

RecyclerView's layoutManager is flexible, with two layouts (horizontal and vertical) and two layouts (forward and reverse). This method takes into account both layout mode and layout direction when calculating targetPosition. Layout mode can be judged by layoutManager.canScrollHorizontally()/layoutManager.canScrollVertically(), and layout direction can be judged by the computeScrollVectorForPosition() method in RecyclerView.SmoothScroller.ScrollVectorProvider interface.

So SnapHelper, in order to adapt to various situations of layoutManager, specifically requires that only layoutManager that implements RecyclerView.SmoothScroller.ScrollVectorProvider interface can use SnapHelper to assist scrolling alignment. The official Linear Layout Manager, GridLayout Manager and Staggered Grid Layout Manager all implement this interface, so they all support SnapHelper.

These methods use Orientation Helper as a tool class when calculating location. It is an auxiliary class used by Layout Manager to measure child ren. It can calculate the size and location of ItemView according to Layout Manager's layout mode and direction.

From the source code, you can see that findTargetSnapPosition() first finds the snapView on the interface when the fling operation is triggered (because the findTargetSnapPosition() method is called in the onFling() method), gets the corresponding snapPosition, and then estimates the position offset by estimateNextPositionDiffForFling(), snapPosit The position offset is added to the ion to get the position at the end of the final scroll, which is the targetSnapPosition.

One thing to note here is that before looking for target SnapPosition, you need to find a reference location, which is snapPosition. This is because the different ItemView positions on the current interface are quite different. Using snapPosition as the reference position will make the target SnapPosition obtained by the reference position plus the position offset closest to the target coordinate position, so that the subsequent coordinate alignment adjustment will be more natural.

See how the estimateNextPositionDiffForFling() method estimates position offsets:

    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper, int velocityX, int velocityY) {
      //Calculate the total rolling distance, which is affected by the speed at which fling is triggered
        int[] distances = calculateScrollDistance(velocityX, velocityY);
      //Calculate the length of each ItemView
        float distancePerChild = computeDistancePerChild(layoutManager, helper);
        if (distancePerChild <= 0) {
            return 0;
        }
      //In fact, this is based on the horizontal layout or vertical layout, to take the corresponding layout direction of the rolling distance.
        int distance =
                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
      //The positive and negative symbols of distance denote the rolling direction and the numerical values denote the rolling distance. In horizontal layout, the content scrolls from right to left to be positive; in vertical layout, the content scrolls from bottom to top to be positive.
      // The rolling distance / item length = the number of rolling items, where the integral part of the calculation results is taken
      if (distance > 0) {
            return (int) Math.floor(distance / distancePerChild);
        } else {
            return (int) Math.ceil(distance / distancePerChild);
        }
    }

You can see that by dividing the total rolling distance by the length of itemview, you can estimate the number of items that need to be rolled, which is the displacement of position. The rolling distance is obtained by SnapHelper's calculateScrollDistance () method, and the length of ItemView is calculated by the computeDistance PerChild () method.

Look at these two approaches:

private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
                                          OrientationHelper helper) {
        View minPosView = null;
        View maxPosView = null;
        int minPos = Integer.MAX_VALUE;
        int maxPos = Integer.MIN_VALUE;
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return INVALID_DISTANCE;
        }

        //Loop through the itemView of the layoutManager to get the minimum position and the maximum position, as well as the corresponding view
        for (int i = 0; i < childCount; i++) {
            View child = layoutManager.getChildAt(i);
            final int pos = layoutManager.getPosition(child);
            if (pos == RecyclerView.NO_POSITION) {
                continue;
            }
            if (pos < minPos) {
                minPos = pos;
                minPosView = child;
            }
            if (pos > maxPos) {
                maxPos = pos;
                maxPosView = child;
            }
        }
        if (minPosView == null || maxPosView == null) {
            return INVALID_DISTANCE;
        }
        //The minimum and maximum locations are definitely located at both ends of the layoutManager, but it is not possible to directly determine which is at the beginning and which is at the end (because of the positive and negative layout).
        //So take the smaller coordinate of the middle point of the two as the starting point coordinate.
        //The same value of the terminal coordinates
        int start = Math.min(helper.getDecoratedStart(minPosView),
                helper.getDecoratedStart(maxPosView));
        int end = Math.max(helper.getDecoratedEnd(minPosView),
                helper.getDecoratedEnd(maxPosView));
        //The total length of these itemview s is obtained by subtracting the starting coordinates from the end coordinates
        int distance = end - start;
        if (distance == 0) {
            return INVALID_DISTANCE;
        }
        // Total length / number of itemviews = average length of itemview
        return 1f * distance / ((maxPos - minPos) + 1);
    }

It can be found that the computeDistancePerChild () method also divides the total length by the number of ItemViews to get the average length of ItemView, and also supports different layout modes and directions of layoutManager.

    public int[] calculateScrollDistance(int velocityX, int velocityY) {
        int[] outDist = new int[2];
        //mGravityScroller is a Scroller. Flying operation is simulated by fling () method. By setting the starting position to zero, the end position is the rolling distance.
        mGravityScroller.fling(0, 0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        outDist[0] = mGravityScroller.getFinalX();
        outDist[1] = mGravityScroller.getFinalY();
        return outDist;
    }

CalulateScrollDistance () is a method in SnapHelper. mGravityScroller is a Scroller object initialized in attachToRecyclerView(). Flying operation is simulated by Scroll. fling () method. The starting position of fling is set to 0, and the end position is the distance of fling. This distance will be divided into positive and negative symbols, indicating the direction of rolling.

Now you see, the main function of LinearSnapHelper is to help RecyclerView scroll Item to align the center position by implementing three abstract methods of SnapHelper.

Custom SnapHelper

After the above analysis and understanding the working principle of SnapHelper, the custom SnapHelper is more comfortable. Now let's look at the effect of the main interface of Google Play.

You can see that this effect is a horizontal list sliding control similar to Gallery, which can obviously be implemented with RecyclerView, while the scrolled ItemView aligns the left edge position of RecyclerView. This alignment effect is implemented with SnapHelper when it is still not allowed. Here's how to implement this SnapHelper.

Create a Gallery SnapHelper that inherits SnapHelper's three abstract methods to implement it:

  • calculateDistanceToFinalSnap (): Calculates the distance between the current SnapView location and the target location
    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
        return out;
    }
  //The difference between the start coordinate of targetView and the padding Start of RecyclerView
  //It's the distance that needs to be adjusted by rolling.
    private int distanceToStart(View targetView, OrientationHelper helper) {
        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
    }

  • findSnapView (): Find SnapView at the current moment.
  @Override
      public View findSnapView(RecyclerView.LayoutManager layoutManager) {
          return findStartView(layoutManager, getHorizontalHelper(layoutManager));
      }

  private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (layoutManager instanceof LinearLayoutManager) {
          //Find the location of the first visible ItemView
            int firstChildPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
            if (firstChildPosition == RecyclerView.NO_POSITION) {
                return null;
            }
          //Find the last fully displayed ItemView if it is the last in the list
              //This means that the list has slid to the end, and then it should not be aligned according to the first ItemView.
              //Otherwise, the last ItemView may never be fully displayed because it needs to be aligned with the first ItemView.
              //So at this point, returning null directly means no alignment is required.
            if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {
                return null;
            }

            View firstChildView = layoutManager.findViewByPosition(firstChildPosition);
          //If the length of the first ItemView is not more than half, take the ItemView as snapView.
          //More than half, use the next ItemView as snapView
            if (helper.getDecoratedEnd(firstChildView) >= helper.getDecoratedMeasurement(firstChildView) / 2 &amp;&amp; helper.getDecoratedEnd(firstChildView) > 0) {
                return firstChildView;
            } else {
                return layoutManager.findViewByPosition(firstChildPosition + 1);
            }
        } else {
            return null;
        }
    }

  • findTargetSnapPosition(): Find targetSnapPosition when fling is triggered.
    @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;

        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd == null) {
            return RecyclerView.NO_POSITION;
        }

        int deltaJump;
        if (layoutManager.canScrollHorizontally()) {
            deltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
            if (vectorForEnd.x < 0) {
                deltaJump = -deltaJump;
            }
        } else {
            deltaJump = 0;
        }

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

This approach is basically the same as the implementation of LinearSnapHelper.

After realizing three abstract methods in this way, we can see the effect:

It's found that it can basically align the left edge as Google Play does. But as a programmer with ideal, culture and pursuit, how can it be so easy to satisfy? The ultimate goal is the ultimate goal! There's no time to explain. Get on the bus!

There are two main differences between the current effect and that in Google Play:

  • The scrolling speed is significantly slower than the horizontal list scrolling speed of Google Play, which makes the scrolling feel more procrastinating and does not look very crisp.

  • Google Play's horizontal list scrolls at most one page of Item, and the current effect will roll far when it slides faster.

In fact, if you understand the principle of SnapHelper I mentioned above, it will be easy to solve these two problems.

For the problem of slow scrolling speed, because this fling process is controlled by SnapHelper's SmoothScroller, we mentioned SmoothScroller's calculateSpeedPerPixel() method when we analyzed the creation of SmoothScroller object is defining the scrolling speed, and the createSnapScroll () method of copying SnapHelper is redefined. Wouldn't it be OK to define a SmoothScroller? !

    //In SnapHelper, the value is 100, and here it is 40.
    private static final float MILLISECONDS_PER_INCH = 40f; 
    @Nullable
    protected LinearSmoothScroller createSnapScroller(final RecyclerView.LayoutManager layoutManager) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return null;
        }
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }

            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }

As you can see, the code is exactly the same as in SnapHelper, just changing the value of MILLISECONDS_PER_INCH to make the return value of calculateSpeedPerPixel() smaller, which makes SmoothScroller scroll faster.

For the problem of scrolling too many Items at a time, it is necessary to limit the number of Items scrolled at a time. Where do we limit the number of rolls? findTargetSnapPosition() method! The purpose of this method is to find where to scroll to and where else to scroll? Look directly at the code:

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
       ...

        //Calculate the number of item s on a screen
        int deltaThreshold = layoutManager.getWidth() / getHorizontalHelper(layoutManager).getDecoratedMeasurement(currentView);

        int deltaJump;
        if (layoutManager.canScrollHorizontally()) {
            deltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
          //Threshold judgment of estimated position offset can only scroll the number of Item s on one screen at most.
            if (deltaJump > deltaThreshold) {
                deltaJump = deltaThreshold;
            }
            if (deltaJump < -deltaThreshold) {
                deltaJump = -deltaThreshold;
            }
            if (vectorForEnd.x < 0) {
                deltaJump = -hDeltaJump;
            }
        } else {
            deltaJump = 0;
        }

        ...
    }

You can see that it's just a matter of limiting the size of the estimated position offset. It's so simple!

With this adjustment, the effect is basically the same as that of Google Play, and I guess that's what Google Play does! See the effect:

Last

If you think the article is well written, give it a compliment? If you think it's worth improving, please leave me a message. We will inquire carefully and correct the shortcomings. Thank you.

I hope you can forward, share and pay attention to me, and update the technology dry goods in the future. Thank you for your support! ___________

Forwarding + Praise + Concern, First Time to Acquire the Latest Knowledge Points

Android architects have a long way to go. Let's work together.

The following wall cracks recommend reading!!!

Finally, I wish you all a happy life.~

Tags: Google Android

Posted on Thu, 08 Aug 2019 03:39:07 -0700 by rel