A custom LayoutManager case: Bumble Beeline

Emre Babur
Bumble Tech
7 min readJul 17, 2019

--

Task

In our Bumble app, we had decided to develop a feature to allow our users to see other users who already liked them. This feature would save them a lot of time by directly creating matches instead of going through a list of users who may or may not have liked them at the time.

More specifically, this feature would display a list of users with indefinite loading as the list scrolls to the end until it runs out of users. Hence, I decided to use RecyclerView to make use of all the great benefits of recycling the child views as they go in and out of the display to create a smooth and performant experience when scrolling through the admiring users.

However, the task at hand required then a specific logic to layout the child views within RecyclerView (see illustration to the left). As RecyclerView.LayoutManager is responsible for laying out the child views and performing the scrolling behaviour, it was clear that our custom layout logic needed to be used in our LayoutManager.

Android already provides developers with three stock LayoutManager implementations: LinearLayoutManager, GridLayoutManager, and StaggeredGridLayoutManager. Although going with one of these bullet-proof implementations is the best option, the following requirements made it impossible for us to use any of them:

  • A view can occupy either half or full width (GridLayoutManager can handle this).
  • A half width occupying view can be placed on either the left or right half.
  • A view can be either in the background or in the foreground (Pink views are in the background while others are in the foreground).
  • A view might be overlapped vertically by either or both previous and next views (see the left-hand side illustration where blue items overlapped pink views are denoted by darker shade and also blue items overlapped each other by half of their height).

So we opted to implement a custom LayoutManager that would encapsulate our custom layout logic.

Getting Started

For tutorial purposes, we will start with a custom vertical linear layout manager implementation and modify it as we progress until it reaches its final form with our custom layout logic.

When en route to implementing a custom LayoutManager there are a few requirements to be fulfilled in order to have a fully-functioning component:

  • Initial layout
  • Scrolling views
  • Recycling views

A custom LayoutManager extends RecyclerView.LayoutManager. This abstract class requires only one method to be implemented: generateDefaultLayoutParams(). This method will provide the default layout parameters for child views in our RecyclerView.

Initial Layout

The first task of our custom LayoutManager is to display items when RecyclerView makes a call to onLayoutChildren() method.

Firstly, we need to calculate the fill area required to display the child views and then lay one after another until we run out of available vertical space or items.

Extracted filling screen top to bottom to a method called fillBottom().

Scrolling

Implementing onLayoutChildren() gives you a screen filled with items but you will soon realise that it won’t do much more than that. To be able to scroll our content we need to do a bit more.

First, we need to decide which directions should be enabled for scrolling by overriding canScrollVertically() and/or canScrollHorizontally(). By default these methods return false and as we need only vertical scrolling we will override canScrollVertically().

override fun canScrollVertically(): Boolean = true

Second, we need to implement the actual scrolling by overriding scrollVerticallyBy(). Note that there is also scrollHorizontallyBy() for horizontal scrolling behaviour.

This method works as follows:

  1. RecyclerView makes a call to scrollVerticallyBy() and provides dy, distance of scroll/fling action.
  2. LayoutManager handles scroll/fling and reports back the actual distance that has scrolled. If no more views are available, actual distance can be less than requested distance.
  3. RecyclerView compares the distances to realise if the end of content has been reached then, and if so, signals this to the user with an edge glow effect.

This method has two parts, as the logic for scrolling down and scrolling up differs slightly. The common logic is that the items should be moved by the desired distance and any empty space created should be filled with items. Finally, the views that have gone off of the screen will be recycled.

Recycling Views

The core idea of RecyclerView is to recycle the views that are no longer displayed anymore so we can reuse them and avoid redundant view inflation.

Our logic here is quite simple, we go through all attached child views and recycle the ones that are out of view bounds.

So far, we have implemented a custom vertical linear layout manager. Despite being a really basic implementation, it clearly explains the concepts that make a LayoutManager work:

  • It fills the screen with just enough items by measuring and laying them out.
  • It allows scrolling by moving items up and down by the correct distance.
  • It fills the empty space after scrolling if necessary.
  • It removes child views that go off the screen by recycling them.

Customisation

Now we need to modify our layout logic to fulfil our custom requirements. For this we are going to need extra information about any given view, like how much vertical overlay it allows, whether it is in the background or foreground etc. Similar to GridLayoutManager.SpanSizeLookup I created an interface called ConfigLookup and extended RecyclerView.LayoutParams to retain this config info per view.

Layout Logic

We will only modify how items are laid out within the RecyclerView in terms of their vertical alignment, width occupation, z-index, and horizontal gravity. Remaining logic for scrolling and recycling views will stay unchanged.

Vertical Alignment

In terms of keeping track of vertical level there are two variables I made use of:

  • Hard line: under any circumstances any consecutive view can never overlap beyond this line.
  • Soft line: this variable only indicates where this view actually ends and so should the consecutive views want to overlap their positions this should be relative to this line.

Items can be solid or not, the difference here is that the solid ones have a hard line that is denoted as higher than their soft line as much as their vertical overlay ratio allows. Non-solid ones do not move the hard line but only move the soft line. For example, Item 2 did not move the hard line as it is a non-solid item.

Vertical overlap is calculated as a minimum of two items’ overlap allowances. This is how we do it. We take whichever is the lowest on the screen: either the hard line, or a little bit above the soft line. This “little bit” is calculated as the next item’s height*overlap ratio. You can see this from item 2 to item 3. Even though the hard line allows item 3 to go up as high as to top of pink item, it only overlaps to only half of its own height as it has an overlap ratio of 0.5.

We also modified fillTop() method which is just a mirrored version of fillBottom().

Horizontal Alignment

To measure sizes correctly for horizontal alignment, we need to provide measureChildWithMargins() with how much of the available width is already being used. For this, I created an extension method to measure the views.

columnWidth here is a value that is equal to half the width of attached RecyclerView.

While laying out the view we set its translationZ to denote its foreground/background status. Also the boundaries for this view are determined according to its gravity, either left or right.

Extras

Save/Restore State

We also need to save/restore our state in order to remember scroll position and offset in case the view is destroyed and created again.

We also need to modify the following lines in fillBottom():

...
} else {
startPosition = if (anchorPosition < adapterItemCount) anchorPosition else 0
top = parentTop + if (anchorPosition < adapterItemCount) anchorOffset else 0
}

Scroll to Specific Position

We were also required to be able to scroll to a specific position and for this we needed to override scrollToPosition() which internally sets the anchor position to the desired position and requests layout which triggers a call to our onLayoutChildren() method.

Conclusion

Even though it might have seemed a bit cryptic at first, I wanted to show you how straightforward it was to obtain the desired result by first creating a very basic custom LayoutManager and then modifying its logic to fulfil the custom requirements. However, if you check the code for stock LayoutManagers though, you will see that there are a lot more optimisations and handled edge cases. Therefore, I would suggest using one of them as long as it has what it takes to handle your layout logic.

You can find the whole BeelineLayoutManager here.

--

--