Can RecyclerView still be scroll-aligned like this?

Estimated read time 5 min read

Preface

If RecyclerView wants to scroll to a specified position, there are generally two methods: scrollToPosition()and . smoothScrollToPosition()After scrolling to the specified position, itemView is usually required to align with the starting point, center point or end point of RecyclerView.

People who are familiar with RecyclerView should know that using customization SmoothScrollercan achieve smooth scrolling to the specified position while aligning the itemView and RecyclerView; while scrollToPosition()the method can only scroll to the specified position. Is there any way to scrollToPosition()achieve alignment?

dismantling behavior

After analyzing the behavior of alignment, it can be divided into several steps

  1. Make the target itemView visible
  2. Calculate the offset between itemView and destination position
  3. Move itemView to destination

The first step scrollToPosition()can already be implemented, and the last step is to call scrollBy(). In fact, you only need to implement the second step to calculate the offset, and you can refer to SmoothScrollerthe implementation of

smooth scrolling

Let’s see SmoothScrollerhow it’s done. Usually the approach is to customizeLinearSmoothScroller

RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int preference = LinearSmoothScroller.SNAP_TO_START;// 对齐方式
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(context){
    @Override
    protected int getHorizontalSnapPreference() {
        return preference;
    }

    @Override
    protected int getVerticalSnapPreference() {
        return preference;
    }
};
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);

Briefly introduce several alignment methods:

  • SNAP_TO_START: Align the starting position of RecyclerView
  • SNAP_TO_END: ​​Align the end position of RecyclerView
  • SNAP_TO_ANY: Align the RecyclerView anywhere to ensure that the itemView is within the RecyclerView

Next, let’s take a look at how the return value of getVerticalSnapPreference()or getHorizontalSnapPreference()affects the alignment of itemView. Check LinearSmoothScrollerthe source code and find that these two methods will be onTargetFound()called in

protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
    final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
    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);
    }
}

It is not difficult to see that this method is to calculate the offset and duration of the targetView’s current scrolling, and set it to the action. And calculateDxToMakeVisible()sum calculateDyToMakeVisible()is exactly what we are looking for to calculate the offset.

Since these two methods only depend on each other LayoutManager, we can copy the code logic and create a Rangefinderclass for calculating offsets.

public class Rangefinder {
    private final RecyclerView.LayoutManager mLayoutManager;

    public Rangefinder(RecyclerView.LayoutManager layoutManager) {
        mLayoutManager = layoutManager;
    }

    @Nullable
    public RecyclerView.LayoutManager getLayoutManager() {
        return mLayoutManager;
    }

    // 计算view在RecyclerView中完全可见所需的垂直偏移量
    public int calculateDyToMakeVisible(View view, int snapPreference) {
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager == null || !layoutManager.canScrollVertically()) {
            return 0;
        }
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
        final int start = layoutManager.getPaddingTop();
        final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
        return calculateDtToFit(top, bottom, start, end, snapPreference);
    }

    // 计算view在RecyclerView中完全可见所需的水平偏移量
    public int calculateDxToMakeVisible(View view, int snapPreference) {
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
            return 0;
        }
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
        final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
        final int start = layoutManager.getPaddingLeft();
        final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
        return calculateDtToFit(left, right, start, end, snapPreference);
    }

    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd,
                                @SnapPreference int snapPreference) {
        switch (snapPreference) {
            case LinearSmoothScroller.SNAP_TO_START:
                return boxStart - viewStart;
            case LinearSmoothScroller.SNAP_TO_END:
                return boxEnd - viewEnd;
            case LinearSmoothScroller.SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
                if (dtStart > 0) {
                    return dtStart;
                }
                final int dtEnd = boxEnd - viewEnd;
                if (dtEnd < 0) {
                    return dtEnd;
                }
                break;
        }
        return 0;
    }
}

With the method of calculating offset, the next step is to realize the alignment of itemView.

Instant scrolling

Based on the above disassembly steps, analyze what needs to be done at each step.

  1. Called scrollToPosition()to make the target itemView visible. Because this method will eventually be used requestLayout(), itemView can only be obtained after layout. Then you can get the itemView by calling the method post()laterLayoutManagerfindViewByPosition()
  2. For reference LinearSmoothScrolleronTargetFound()use the above method Rangefinderto calculate the offset between itemView and destination position.
  3. Call scrollBy()to move the itemView to the destination location
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
recyclerView.scrollToPosition(targetPosition);
recyclerView.post(new Runnable() {
    @Override
    public void run() {
        View targetView = layoutManager.findViewByPosition(targetPosition);
        if (targetView != null) {
            Rangefinder rangefinder = new Rangefinder(layoutManager);
            final int dx = rangefinder.calculateDxToMakeVisible(targetView, preference);
            final int dy = rangefinder.calculateDyToMakeVisible(targetView, preference);
            if (dx != 0 || dy != 0) {
                recyclerView.scrollBy(-dx, -dy);
            }
        }
    }
});

At this point, we have realized the function of aligning itemView and RecyclerView while instantly scrolling to position. Of course, this is just test code, and the above logic will be encapsulated in actual use.

You May Also Like

More From Author

+ There are no comments

Add yours