image
Hello everyone!

My name is Anton Knyazev , senior Android developer at Omega-R. Over the past seven years, I have been professionally developing mobile applications and solving complex problems of native development.

I want to share the RecyclerView extension methods developed by our team and me. They will become a reliable base for creating custom lists in applications.

Each application is unique in its own way due to its idea, design and team of specialists. Successful decisions often want to be transferred from one project to another. Therefore, instead of just copying, it’s logical to create a separate library that the whole team would use and improve.

The team decided to create small libraries that improve and accelerate application development, and upload them to the GitHub public repository . This makes it easy to connect the library in projects through JitPack and gives customers a guarantee that there is nothing “criminal” in the code.

The first library that we posted on GitHub is a simple extension of RecyclerView.

Let's start with the problems she solved:

  1. There is no default layoutManager - this is inconvenient, as you often have to choose the same LinearLayoutManager;
  2. There is no way to add divider and item space via xml - this is also inconvenient, since you need to add either to item layout or through ItemDecorator;
  3. You can’t just add header and footer via xml - this is possible only through a separate ViewHolder.

The problems are uncritical, but create inconvenience and increase development time.

1. Problem: there is no default layoutManager


The developers of RecyclerView did not provide the ability to select the default LayoutManager. You can set layoutManager in the following ways:

1. Via XML in the attribute app: layoutManager=”LinearLayoutManager”:

<?xml version="1.0" encoding="utf-8"?> <androidx.recyclerview.widget.RecyclerView ... app:layoutManager="LinearLayoutManager"/> 

2. Via code:

recyclerView.layoutManager=LinearLayoutManager(this) 

In our experience, in most cases, LinearLayoutManager is needed.

Here are some examples of such listings from our ITProTV, Simple World and Dexen applications:

image

Solution: add default layoutManager


Only 3 lines are added to OmegaRecyclerView:

if (layoutManager == null) { layoutManager=LinearLayoutManager(context, attrs, defStyleAttr, 0) } 

Thus, when LinearLayoutManager is required, you do not need to add anything, that is, you can forget about layoutManager.

<?xml version="1.0" encoding="utf-8"?> <com.omega_r.libs.omegarecyclerview.OmegaRecyclerView android:id="@+id/recyclerview" android:layout_width="match_parent" android:layout_height="match_parent"/> 

2. Problem: there is no way to add divider and item space via xml


You have to add divider quite often when using RecyclerView. For example, in the project “Simple World” one of the screens was with such a non-standard divider:

image

This layout shows that:

  • used by the divider between the elements and at the very end;
  • used item space.

How can this be implemented in Android in a standard way?

Method 1


The most obvious way is to include the divider as an ImageView element:

<RelativeLayout ... android:paddingStart="20dp" android:paddingTop="12dp" android:paddingEnd="20dp" android:paddingBottom="12dp"> ... <ImageView ... android:layout_alignParentBottom="true" android:src="@drawable/divider"/> </RelativeLayout> 

It may happen that you need to do divider only between elements. In this case, you will have to remove the last divider and add the code to hide it in the adapter.

Method 2


Another way is to use the DividerItemDecoration, which this divider can draw.For it, you must additionally create a drawable:

<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:left="32dp"> <shape android:shape="rectangle"> <size android:width="1dp" android:height="1dp"/> <solid android:color="@color/gray_dark"/> </shape> </item> </layer-list> 

To add indentation you need to write your ItemDecoration:

class VerticalSpaceItemDecoration(private val verticalSpaceHeight: Int): RecyclerView.ItemDecoration { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { outRect.bottom=verticalSpaceHeight } } 

DividerItemDecoration is simple: it draws a divider always under each list item.
But if the requirements change, you will have to look for another solution.

Solution: let's add the ability to add divider and item space via xml


So, our OmegaRecyclerView should be able to add divider using the following attributes:

  1. divider - defines drawable, color can also be assigned directly;
  2. dividerShow (beginning, middle, end) - flags that determine where to draw;
  3. dividerHeight - sets the height of the divider, in the case of color it becomes especially necessary;
  4. dividerPadding, dividerPaddingStart, dividerPaddingEnd - indents: general, from the beginning, from the end;
  5. dividerAlpha - defines transparency;
  6. itemSpace - indent between list items.

All of these attributes apply directly to our OmegaRecyclerView using two special ItemDecoration.

One of the ItemDecoration adds padding between items, the second draws the divider itself. Keep in mind that if there is an indent between the elements, the second ItemDecoration draws a divider in the middle of the indent. There is no way to support all possible LayoutManager options, so we only support LinearLayoutManager and GridLayoutManager. Also note the list orientation for LinearLayoutManager.

In order to simplify the code, we select a special DividerDecorationHelper, which will read and write relative data in Rect, depending on the orientation and sequence (reverse or direct). It will have the methods setStart, setEnd and getStart, getEnd.

Let's create a basic ItemDecoration for two classes, which will contain the general logic, namely:

  1. checking that layoutManager is an inheritor of LinearLayoutManager;
  2. calculating the current orientation and order;
  3. determining the appropriate DividerDecorationHelper.

In SpaceItemDecoration we will redefine only one getItemOffset method which will add indents:

override fun getItemOffset(outRect: Rect, parent: RecyclerView, helper: DividerDecorationHelper, position: Int, itemCount: Int) { if (isShowBeginDivider() || countBeginEndPositions <= position) helper.setStart(outRect, space) if (isShowEndDivider() && position == itemCount - countBeginEndPositions) helper.setEnd(outRect, space) } 

The next DividerItemDecoration will draw the divider directly. It should take into account the indent between the elements and draw a divider in the middle. First, we redefine the getItemOffset method for the case when the indent is not specified, but the divider is required for drawing.

override fun getItemOffset(outRect: Rect, parent: RecyclerView, helper: DividerDecorationHelper, position: Int, itemCount: Int) { if (position == 0 && isShowBeginDivider()) { helper.setStart(outRect, dviderSize) } if (position != 0 && isShowMiddleDivider()) { helper.setStart(outRect, dividerSize) } if (position == itemCount - 1 && isShowEndDivider()) { helper.setEnd(outRect, dividerSize) } } 

We also add an option that allows DividerItemDecoration to ask the adapter whether it is possible to draw above or below the selected item. To implement this feature, we will create our own adapter that inherits from the standard one with the following methods:

open fun isDividerAllowedAbove(position: Int): Boolean { return true } open fun isDividerAllowedBelow(position: Int): Boolean { return true } 

Next, we redefine the onDrawOver method to draw the divider on top of the drawn elements. In this method, you need to go through all the elements visible on the screen (via getChildAt) and, if necessary, draw this divider. It should also be taken into account that a color that does not have height can come from the dividerDrawable attribute. For such a case, the height can be taken from the dividerHeight attribute.

3. Problem: you cannot directly add header and footer via xml


image

It is not possible to add view through xml in RecyclerView, but there are other ways to do this.

Method 1


One obvious way to add view is via adapter. Moreover, you must distinguish between header and footer in the adapter when entering your identifier for viewType.

fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? { val inflater=LayoutInflater.from(parent.context) return when (viewType) { TYPE_HEADER -> { val headerView: View=inflater.inflate(R.layout.item_header, parent, false) HeaderViewHolder(itemView) } TYPE_ITEM -> { val itemView: View=inflater.inflate(R.layout.item_view, parent, false) ItemViewHolder(itemView) } else -> null } } 

Method 2


A slightly different way, but also through the adapter. Starting with recyclerview: 1.2.0-alpha02, the MergeAdapter has appeared, which allows you to combine several adapters into one, making the code cleaner.

val mergeAdapter=MergeAdapter(headerAdapter, itemAdapter, footerAdapter) recyclerView.adapter=mergeAdapter 

Solution: add the ability to simply add header and footer via xml


The first thing to do is to intercept the addition of view in our OmegaRecyclerView when the inflate process is in progress. To do this, override the addView method and add all the header and footer view to yourself. This method is used by RecyclerView itself to complement visible list items. But view added via xml will not have a ViewHolder, which will ultimately throw a NullPointerException.

So, we need to determine when view is added during inflate. Fortunately, there is a protected onFinishInflate method that is called when the inflate process ends. Therefore, when calling this method, we mark that the inflate process is completed.

protected override fun onFinishInflate() { super.onFinishInflate() finishedInflate=true } 

Thus, the addView method will look like this:

override fun addView(view: View, index: Int, params: ViewGroup.LayoutParams) { if (finishedInflate) { super.addView(view, index, params) } else {//save header and footer views } } 

Next, you need to remember all these additional views and transfer them to a special adapter of the MergeAdapter type.

We also managed to solve another problem: when the findViewById method is called, our views will not be returned. To solve this problem, redefine the findViewTraversal method: in it, you need to compare the id of the view we found and return the view if it matches. Since this method is hidden, just write it without indicating that it is override.

You can find out about these and other useful features with a detailed description in our library OmegaRecyclerView :

  1. In 2018, we created our ViewPager from RecyclerView. Moreover, in our ViewPager there is an endless scroll;
  2. ExpandableRecyclerView - a special class for adding a drop-down list, with the ability to select the expansion animation;
  3. StickyHeader is a specific list item that can be added via an adapter.

All this is the result of Omega-R experience. The evolution of developer skill goes through several stages. First, you want to copy the code from another project or do something similar to it. Then comes the stage when it is necessary to record the accumulated experience and create a separate repository.

At the next stage, you begin to purposefully create features that hesitated to do in projects. This may take considerable time, but allows you to create a reserve for the future and speed up development in new projects. I invite everyone who has difficulty in developing to get acquainted with our solutions in the GitHub repositories Omega-R.

Source