In this third and final part of the RecyclerView LayoutManager blog we’ll take a look at some of the extra features we added. These are not required to build a LayoutManager, but add some useful functionality.

Part one and two of this series can be found here and here.
A sample project using this layout manager can be found here.

Supporting dataset changes

Items inserted or removed above the visible items will cause the firstPosition value to become incorrect. To fix this we can listen for item addition/removal events and update our firstPosition value if necessary.

@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)
{
    if (positionStart < firstPosition)
    {
        firstPosition += itemCount;
    }
}

@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)
{
    if (positionStart < firstPosition)
    {
        firstPosition -= itemCount;
    }
}

Scrolling to a specific position

Scrolling to a specific position in the feed comes in two flavours.

scrollToPosition

As mentioned in part 1, your LayoutManager should implement the scrollToPosition() method.

/**
 * Jumps to the requested position. Not animated.
 *
 * @param position The adapter position to jump to.
 */
@Override
public void scrollToPosition(final int position)
{
    firstPosition = position;

    // Remove all views so scroll offset is reset and our target view gets its top aligned with the top of the RecyclerView.
    removeAllViews();
    requestLayout();
}

This method is pretty straightforward. We just update the firstPosition value and perform a new layout starting from this new position instead of the default position 0.
We manually remove all views to make the onLayoutChildren() start with a clean slate, without a scroll offset.

smoothScrollToPosition

Besides the mandatory scrollToPosition() method you can also implement the optional, animated version: smoothScrollToPosition(). This method will also move the RecyclerView to a specific position in the feed, but with a scrolling animation.

@Override
public void smoothScrollToPosition(RecyclerView RecyclerView, RecyclerView.State state, int position)
{
    final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(RecyclerView.getContext())
    {
        @Override
        public PointF computeScrollVectorForPosition(int targetPosition)
        {
            if (getChildCount() == 0)
            {
                return null;
            }

            // Determine which direction we need to scroll in (-1 is up, 1 is down)
            final int firstChildPos = getAdapterPositionForviewPosition(0);
            final int direction = targetPosition < firstChildPos ? -1 : 1;

            // Only need to scroll vertically.
            return new PointF(0, direction);
        }

        @Override
        protected int getVerticalSnapPreference()
        {
            return SNAP_TO_START;
        }
    };
    linearSmoothScroller.setTargetPosition(position);
    startSmoothScroll(linearSmoothScroller);
}

You can use the provided LinearSmoothScroller class to performs the actual scrolling for you. All you need to do is implement its computeScrollVectorForPosition() method to let the LinearSmoothScroller know in which direction it needs to start scrolling. While scrolling it will continuously check to see if the requested position is visible on screen. Once the target has been found the LinearSmoothScroller will start decelerating and stop at the requested item.

The getVerticalSnapPreference() method is implemented here to let the LinearSmoothScroller know we always want to align the top of the target item with the top of the RecyclerView, regardless of the scroll direction.

Retrieving the on screen positions

The Linear- and GridLayoutManager have several methods to find out which item positions are currently displayed on screen:

  • findFirstVisibleItemPosition()
  • findFirstCompletelyVisibleItemPosition()
  • findLastVisibleItemPosition()
  • findLastCompletelyVisibleItemPosition()

We use these for analytics and to load the next page of search results when the user approaches the bottom of the feed.

Implementing these yourself is very easy:

/**
 * @return The adapter position of the first view that is (partially) visible on screen.
 */
public int findFirstVisibleItemPosition()
{
    return getChildCount() > 0 ? getPosition(getChildAt(0)) : 0;
}

/**
 * @return The adapter position of the first view that is completely visible on screen.
 */
public int findFirstCompletelyVisibleItemPosition()
{
    if (getChildCount() > 0)
    {
        for (int i = 0; i < getChildCount(); i++)
        {
            final View v = getChildAt(i);
            if (getDecoratedTop(v) >= getPaddingTop() && getDecoratedBottom(v) <= getHeight() - getPaddingBottom())
            {
                return getPosition(v);
            }
        }
    }

    return 0;
}

// Similar for the last visible position

Item decorations

Almost all of the RecyclerViews in our apps use an ItemDecoration to add margins between or around the items in the list. This cleans up the XML layouts of the individual items, makes it easier to dynamically update margins and makes it more obvious where margins are coming from.

Supporting item decorations in a custom LayoutManager is virtually free. It only requires you to use getDecoratedLeft(view), getDecoratedTop(view),… instead of the standard view.getLeft(), view.getTop(),… methods.

The following image shows why this is important. A full width item has been given a 40 pixel decoration margin on each side. The screen is 1440 pixels wide and the item 1360 after its decorations have been applied.

Decorations

Calling view.getLeft on a view will give you its position after the decorations have been applied, which in this case would be 40. This is the value you’d want in some cases, but the LayoutManager calculations should be based on the size of the item with its ItemDecoration margins included.
Calling getDecoratedLeft(view) gives you exactly that. In this case this would give us 0, the value we’d use for positional calculations.

Limitations

This LayoutManager met our requirements, but does have its limitations. These were not required in our app and therefore not implemented.

Strict item order

Items in the dataset have to be in the exact same order that they will appear on screen. This means a TRIO must first have the small top item followed by the large item and finally the bottom small item. Any other order would create gaps in the layout order, leading to several issues. An example of this is the recycleViewsOutOfBounds() method which assumes all visible views will be one continuous series. Gaps would cause all view outside of this range to be removed, even if they were still visible on screen.

Unable to remove items from DUO and TRIO groups

UNO items can be added or removed, but the same is not possible for items that are part of a DUO or TRIO. The boundaries between these groups would become unclear leading to inconsistent behaviour and incorrect layouts.

scrollToPosition interaction with groups

scrollToPosition() only works when jumping to the first item of a group (left item in a DUO, top small one in a TRIO). Because it updates the firstPosition value and triggers a new layout starting from that position, it won’t add the preceding items in that group.
smoothScrollToPosition() works fine for any position because it uses the same logic as regular user scroll/fling events.

Conclusion

While our LayoutManager is not super advanced or rich in features, it did solve our biggest problem in the AD.nl app. Creating a custom LayoutManager allowed us to split our feed into the smallest possible chunks, leading to code that was easier to understand and a lot more pleasant to maintain.

Creating a LayoutManager is not easy; it takes time and can be tricky to get right. Small mistakes can lead to unpredictable and hard to debug behaviour.

Do give it a try if you ever need to implement a feed that goes beyond what the standard Linear or Grid layouts can provide. This was a big learning experience, but it has given us a better understanding of the RecyclerView and more confidence to face complex feed structures in the future.