Android: RecyclerView and its Custom LayoutManager

It has been a few months now since the RecyclerView was introduced by Google last year, and finally I got a chance to play with it. This article is mainly focused on building a custom LayoutManager for the RecyclerView. I’ll skip most of fundamental parts of RecyclerView because there are many such tutorials already available on the Internet.

Starting with basics:

Per Google’s definition, RecyclerView is “a flexible view for providing a limited window into a large data set”. In other words, It is a more advanced and flexible version of ListView, or more generally speaking, it’s a CollectionView that handles a collection of child views. Everything you do with ListView you can also use RecyclerView to replace, but with more flexible layout management and animation effects. If you haven’t learnt all these stuff, I suggest to take a look at this tutorial.

Similar to ListView, a RecyclerView requires a adapter to bind data to views. In this case, the adapter needs to extend RecyclerView.Adapter class. In addition, you need to set a LayoutManager to your RecyclerView for measuring and laying out all child views within the RecyclerView. This step seems to be extra work appending to ListView, but it gives you more power to handle the layout to look exact like the way you want it. The LayoutManager also needs to determine the right time to recycle a view when it’s not visible anymore. We will discuss this in the following sample.

A sample adapter implementation is already listed in the tutorial mentioned above, take a close look then you will realize it’s just like the adapter for ListView, but enforces the ViewHolder mechanism. It’s very clear to follow so I will skip this part.

By default, Android supports 3 LayoutManagers: LinearLayoutManager, GridLayoutManager, and StaggeredGridLayoutManager. What if you want something more flexible? You can implement your own LayoutManager by extending RecyclerView.LayoutManager. Next let’s walk through a sample custom LayoutManager that places child views at predefined locations, while recycle views that are no longer visible and re-use them when a same type of view need to be shown.

After you created your LayoutManager class, the only required override method is:

@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT);
}

It basically creates a default LayoutParams for the RecyclerView. Now your LayoutManager is compilable!

Starting from here, things get more interesting. The next important method you probably want to override is:

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

}

this method is called when the child views are initially placed, and when the adapter’s dataset changes. In our sample case, we want it to layout all visible children.

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    fillVisibleChildren(recycler);
}

private void fillVisibleChildren(RecyclerView.Recycler recycler){
    //before we layout child views, we first scrap all current attached views
    detachAndScrapAttachedViews(recycler);

    //layoutInfo is a Rect[], each element contains coordinates for a view.
    for(int i = 0; i < layoutInfo.length; i++){
        if(isVisible(i)){
            View view = recycler.getViewForPosition(i);
            addView(view);
            layoutDecorated(view, layoutInfo[i].left, layoutInfo[i].top - verticalScrollOffset, layoutInfo[i].right, layoutInfo[i].bottom - verticalScrollOffset);
        }
    }
}

/*determine whether a child view is now visible
**getVerticalSpace() and getHorizontalSpace() returns layout space minus paddings.
**verticalScrollOffset and horizontalScrollOffset are current offset distances
**according to the initial left top corner, before any scrolling.
*/
private boolean isVisible(int index){
    if(layoutInfo[index].bottom < verticalScrollOffset
            || layoutInfo[index].top > getVerticalSpace() + verticalScrollOffset
            || layoutInfo[index].right < horizontalScrollOffset
            || layoutInfo[index].left > getHorizontalSpace() + horizontalScrollOffset){
        return false;
    }
    else{
        return true;
    }
}

At this point, you already have the initial layout setup, all visible child views are now on your screen!

The next step is to enable scrolling, this is a RecyclerView after all!

To enable scrolling, you need to override the following two methods, depending on your scroll direction:

@Override
public boolean canScrollHorizontally() {
    return true;
}

@Override
public boolean canScrollVertically() {
    return true;
}

This will give your layout scrolling ability, but we need to tell the LayoutManager what to change after we scroll. Let’s keep going, override the following methods:

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    int travel;
    final int leftLimit = 0;
    final int rightLimit = findRightLimit(); //a helper method to find the rightmost child's right side.
    if(dx + horizontalScrollOffset < leftLimit){
        travel = horizontalScrollOffset;
        horizontalScrollOffset = leftLimit;
    }
    else if(dx + horizontalScrollOffset + getHorizontalSpace() > rightLimit){
        travel = rightLimit - horizontalScrollOffset - getHorizontalSpace();
        horizontalScrollOffset = rightLimit - getHorizontalSpace();
    }
    else{
        travel = dx;
        horizontalScrollOffset += dx;
    }
    fillVisibleChildren(recycler);
    return travel;
}

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {

    int travel;
    final int topLimit = 0;
    final int bottomLimit = findBottomLimit();//a helper method to find the bottommost child's bottom side.
    if(dy + verticalScrollOffset < topLimit){
        travel = verticalScrollOffset;
        verticalScrollOffset = topLimit;
    }
    else if(dy + verticalScrollOffset + getVerticalSpace() > bottomLimit){
        travel = bottomLimit - verticalScrollOffset - getVerticalSpace();
        verticalScrollOffset = bottomLimit - getVerticalSpace();
    }
    else{
        travel = dy;
        verticalScrollOffset += dy;
    }
    fillVisibleChildren(recycler);
    return travel;
}

The dx and dy parameter in these two methods are the distances to scroll in pixel. dx increases as scroll position approaches the right. dy increases as scroll position approaches the bottom. These two methods return the actual distance travelled. As boundary limitations, the returned distance may be smaller than dx or dy. We have to handle the boundary cases by ourselves, simple math though.

getVerticalSpace();
getHorizontalSpace();

returns the available space of the visible area, for example, a normal getVerticalSpace() implementation returns  getMeasuredHeight() – getPaddingTop() – getPaddingBottom().

verticalScrollOffset and horizontalScrollOffset are local variables that keep a record of the current scrolling offset. Every time you scroll the view these values get updated. These values are used when we calculate if a child view is currently visible. See

private boolean isVisible(int index)

above for example.

After we return the actual scrolled distance, we update locations for all child views, by calling fillVisibleChildren() again.

Now you have your first custom LayoutManager for RecyclerView. See how simple it is!

If you are interested in more complex cases: you can take a look at this blog, which introduce a custom grid LayoutManager.

Advertisements

3 thoughts on “Android: RecyclerView and its Custom LayoutManager

  1. We must manually move the views ourselves inside of scrollVerticallyBy() and scrollHorizontalBy(). The offsetChildrenVertical() and offsetChildrenHorizontal() methods assist us in applying the uniform translation. If you don’t do this, your views won’t scroll.

  2. We must manually move the views ourselves inside of scrollHorizontalBy() . The offsetChildrenVertical() and offsetChildrenHorizontal() methods assist us in applying the uniform translation. If you don’t do this, your views won’t scroll.

  3. I’m sorry for I don’t quite understand where can I get vertical/horizontal space and scroll offset values. Can you point me to the right direction?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s