Adapter / LayoutManager / LinearLayoutManager / LinearSmoothScroller / RecyclerView / SmoothScroller

Scrolling RecyclerView – Part 3

In the previous article we did an exploration in to how smooth scrolling in LinearLayoutManager is performed in order to understand why calling smoothScrollToPosition() on RecyclerView does not permit us to specify a duration for the scroll. In this article we’ll look at how we can customise this behaviour given we understand how the list items will be used within the RecyclerView.

First let’s consider our list items. We’re using a standard android.R.layout.simple_list_item_1 layout for each list item, and the text itself it relatively short – ‘Item 1000’ will be the longest string that we have to fit in, and this isn’t going to wrap even on the smallest of displays. Therefore we can have a high degree of confidence that each list item will have a consistent height. Knowing this gives us something of an advantage because, if we know the height of all list items, we can determine the total number of pixels we must scroll through to reach the end position.

So first we need to subclass LinearLayoutManager so that we can override smoothScrollToPosition():

public class ScrollingLinearLayoutManager extends LinearLayoutManager {
    private final int duration;

    public ScrollingLinearLayoutManager(Context context, int orientation, boolean reverseLayout, int duration) {
        super(context, orientation, reverseLayout);
        this.duration = duration;
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
                                       int position) {
        View firstVisibleChild = recyclerView.getChildAt(0);
        int itemHeight = firstVisibleChild.getHeight();
        int currentPosition = recyclerView.getChildPosition(firstVisibleChild);
        int distanceInPixels = Math.abs((currentPosition - position) * itemHeight);
        if (distanceInPixels == 0) {
            distanceInPixels = (int) Math.abs(firstVisibleChild.getY());
        }
        SmoothScroller smoothScroller = new SmoothScroller(recyclerView.getContext(), distanceInPixels, duration);
        smoothScroller.setTargetPosition(position);
        startSmoothScroll(smoothScroller);
    }
    .
    .
    .
}

In smoothScrollToPosition() we get the first visible child from the RecyclerView, determine its height and current position and, from that information, and the target position we can determine the number of pixels that we need to scroll through. There’s an edge-case of if the first item is partially scrolled off the top of the screen will give a position delta of 0, so we just set the distance to the Y offset of the first visible child in this case.

It’s worth pointing out that we use absolute values throughout. We’re not interested in the direction of scrolling – that’s already handled for us. We merely need to calculate the duration, so the total number of pixels that we have to traverse is what is important here.

So now we need to subclass LinearSmoothScroller to override the scroll duration calculation:

public class ScrollingLinearLayoutManager extends LinearLayoutManager {
    .
    .
    .
    private class SmoothScroller extends LinearSmoothScroller {
        private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
        private final float distanceInPixels;
        private final float duration;

        public SmoothScroller(Context context, int distanceInPixels, int duration) {
            super(context);
            this.distanceInPixels = distanceInPixels;
            float millisecondsPerPx = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
            this.duration = distanceInPixels < TARGET_SEEK_SCROLL_DISTANCE_PX ? 
                (int) (Math.abs(distanceInPixels) * millisecondsPerPx) : duration;
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return ScrollingLinearLayoutManager.this
                    .computeScrollVectorForPosition(targetPosition);
        }

        @Override
        protected int calculateTimeForScrolling(int dx) {
            float proportion = (float) dx / distanceInPixels;
            return (int) (duration * proportion);
        }
    }
}

So we’re not changing the actual behaviour – SmoothScroller will still perform a series of 10000 pixel flings, we’re just altering how the duration of each of these flings is calculated.

In the constructor we just perform a check to see if the scroll amount is less that a single fling, and if so we just perform a fixed speed scroll, otherwise we set the total duration for the entire scroll operation.

When there are multiple flings calculateTimeForScrolling() gets called for each fling operation to determine its duration. All we need to do here is calculate the proportion of the total distance that the fling will cover and allow it the same proportion of the total duration.

So when we can now see that the scrolling is much faster while still behaving well over short scrolls:

This is all well and good if we know that our list items are a fixed height, but what about if we have multiple different view types within the Adapter, or we know that our View sizes will vary? The key to how to solve this becomes a matter of understanding how your views will work, and perhaps putting some of the logic determining relative pixel distances between two list items in to the Adapter itself.

For example, consider a contacts list with header items marking each different letter. The Adapter contains two distinct View types: The first for individual contacts within the list, and the second for header lines which appear at points where the initial letter changed. If all of the header items are of a fixed size, and the contact items are also of fixed but different size to the header items then given a start and end position the Adapter should be able to calculate to total scroll distance in pixels.

For non-uniform list item sizes it should be possible to give an approximate average value (which you should be able to make a guess at by understanding how your layouts will work). In this case you may experience slight variations in the total scroll duration, but overall you should still be able to ensure that a scroll over a long distance does not keep the user waiting for too long.

For really complex lists one approach may be to hold a sparse array full of sizes for each list item within the Adapter. As each list item is bound its size is added to the sparse array. When a smoothScrollToPosition() is invoked, the Adapter calculates an average item size based upon the items it has bound (an therefore knows the size of). So as the scroll is in progress the Adapter is learning as it goes, and can provide an increasingly more accurate average size as the scroll progresses.

Please bear in mind that all of the ideas outlined within this article are specific to the default behaviour of LinearLayoutManager and if you’re using another LayoutManager the scrolling behaviour will be different and may require a different approach.

That concludes our dive in to the scrolling behaviour of LinearLayoutManager, but in the next short series we’ll have a look at an alternative approach in terms of UX which may eliminate the need to use smoothScrollToPosition().

The source code for this article is available here.

© 2015, Mark Allison. All rights reserved.

Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

5 Comments

      1. how to smooth scroll with offset ?? like linearLayoutManager.scrollToPositionWithOffset(0, 150);

        I want to smooth scroll position with offset.

  1. HI nice tutorial, Is it possible to add a recycler view which scroll horizontal, inside an listItem of an listview which scroll vertically..

  2. Thanks for tutorial, It’s help a lot,

    Can you please provide solution for how to scroll only number of amount with recyclerview let’s say once user swipe it scroll 6 item only, same like do in Android Play store.

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.