RecyclerView item optimizations

Or how StaticLayout can help you

Michael Spitsin
7 min readSep 19, 2016

There are tons of articles about how RecyclerView can be scrolled smoothly when you upload items to it. How we can organize their layout, information that images must be downloaded out of the UI thread, information that items must be prepared, so onBindView method will contain only showing new information work and nothing more (some computations or formatting are will be done somewhere else). For example: link, link, link and link.

But there is relatively small amount of articles about how using of custom view as RecyclerView item can bring benefits compared with xml layout. I found only couple of them: here and here.

I know that my article will be not unique, since there are at least two of similar posts, but my aim is to make knowledge about using static layout in RecyclerView is more public, since, for example, I could not find info about them while ‘googling’ something like “recycler view optimization”. Also there are much more posts about optimizing layouts and e.t.c. and much less about static layouts and else small but powerful improvements.

Our standard list

First look at the program that will display simple list of games. Each item will have game icon, full title and small description of the game. We will use requirement that title must use only one line of text. Description can be any size depends on text length. For image loading we will use Picasso and also in project we will have bunch of test images to imitate real data for list. Here is a link to github, that shows first version of that program.

In this simple variant we have all we need to work with list. We have a Game.class that represents content of each item in list and we have GameListAdapter.class to binding content to appropriate children of RecyclerView. Here is a code of creating view holder and binding item to it:

@Override
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.item_game, null, false);
return new GameViewHolder(itemView);
}

@Override
public void onBindViewHolder(GameViewHolder holder, int position) {
Game game = games.get(position);
Picasso.with(holder.icon.getContext())
.load(game.getIconId())
.into(holder.icon);
holder.title.setText(game.getTitle());
holder.description.setText(game.getDescription());
}

Also we have an item_game.xml file with layout of game list item:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:background="@android:color/white"
>

<ImageView
android:id="@+id/icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:src="@drawable/ic_gothic"
/>

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/icon"
android:textSize="22sp"
android:textColor="@color/text_primary"
android:singleLine="true"
/>

<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/icon"
android:layout_below="@id/title"
android:textSize="18sp"
android:textColor="@color/text_secondary"
/>

</RelativeLayout>

Text optimization

It seems pretty standard solution that can be used in most cases, but if we run profiling and see results of tracing, we will notice that one of performance-important points is setting text to TextView by calling TextView.setText(CharSequence) function. If we will look to source code of it we will see that text view uses bunch of preparations steps and also creates one of Layout inheritors: Static and Dynamic layouts.

Profiling results for initial variant (setText is most time consuming method)

So the difference between them is that Dynamic layout for “dynamic” texts (I’m Captain Obvious, I know) and static layout is for immutable texts. As a Layout doc says:

A base class that manages text layout in visual elements on the screen.

For text that will be edited, use a DynamicLayout, which will be updated as the text changes. For text that will not change, use a StaticLayout.

So we will try to use this knowledge. In our example we need to use immutable text and I suspect that it is a real big case. There are a lot of situations, when we need to just show some list of text information without its dynamic updating. So we will replace xml layout with custom view. Thus, we also improve our item hierarchy (one view with Drawable and 2 StaticLayout instead of relative layout with 3 sub views) and avoid xml parsing. Here is a link to that version of program.

In this step of optimization we have no xml layout and we have to change adapter code to:

@Override
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewGroup.LayoutParams params = new RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
GameItemView itemView = new GameItemView(parent.getContext());
itemView.setLayoutParams(params);
return new GameViewHolder(itemView);
}

@Override
public void onBindViewHolder(GameViewHolder holder, int position) {
Game game = games.get(position);
holder.gameItemView.setGame(game);
}

And provide GameItemView (main method for the adapter is setGame; also it implements Target interface to interact with Picasso):

public void setGame(Game game) {
iconDrawable = new ColorDrawable(0x00000000);
iconDrawable.setBounds(iconMargin, iconMargin, iconMargin + iconSize, iconMargin + iconSize);
Picasso.with(getContext()).load(game.getIconId()).into(this);

int textXOffset = 2 * iconMargin + iconSize;
CharSequence truncatedTitle = TextUtils.ellipsize(game.getTitle(), titlePaint, SCREEN_SIZE.x - textXOffset, TextUtils.TruncateAt.END);
titleLayout = new StaticLayout(truncatedTitle, titlePaint, SCREEN_SIZE.x - textXOffset, Layout.Alignment.ALIGN_NORMAL, 1, 1, true);
descriptionLayout = new StaticLayout(game.getDescription(), descriptionPaint, SCREEN_SIZE.x - textXOffset, Layout.Alignment.ALIGN_NORMAL, 1, 1, true);

requestLayout();
invalidate();
}

Now we run application and profiler and see that time was reduced

Also lets see, what we will have, if we will use DynamicLayout instead of StaticLayout:

Time consumption of StaticLayout constructor
Time consumption of DynamicLayout constructor

As you can see, we have slight improvement when using StaticLayout.

Also we will add some small improvement here, because my test images was too large for places where they would be shown. So we need to explicitly resize them:

Picasso.with(getContext()).load(game.getIconId()).resize(iconSize, iconSize).into(this);

Caching StaticLayout

One of advantages of StaticLayout is context independence, so we can easily cache it. I used simple caching mechanism with some code duplication. May be it is not so perfect (in terms of architecture and OOP principles), but it’s just an example to show you optimization points. You can write your own cache or may be use some libraries:

private enum LayoutCache {
INSTANCE;

private int width;
private final LruCache<CharSequence, StaticLayout> titleCache = new LruCache<CharSequence, StaticLayout>(100) {
@Override
protected StaticLayout create(CharSequence key) {
return new StaticLayout(key, titlePaint, width, Layout.Alignment.ALIGN_NORMAL, 1, 1, true);
}
};
private final LruCache<CharSequence, StaticLayout> descriptionCache = new LruCache<CharSequence, StaticLayout>(100) {
@Override
protected StaticLayout create(CharSequence key) {
return new StaticLayout(key, descriptionPaint, width, Layout.Alignment.ALIGN_NORMAL, 1, 1, true);
}
};

public void changeWidth(int newWidth) {
if (width != newWidth) {
width = newWidth;
titleCache.evictAll();
descriptionCache.evictAll();
}
}

public StaticLayout titleLayoutFor(CharSequence text) {
return titleCache.get(text);
}

public StaticLayout descriptionLayoutFor(CharSequence text) {
return descriptionCache.get(text);
}
}

Note that this class is singleton. Moreover it is a singleton with state. I know, I know, I personally wrote about it and strongly urged you not to use Singletons in Android or at least think twice about it. But, I think in this example it is quite reasonable to use singleton because it’s a cache. It’s not so harm to lose data in it. This class can automatically generate new StaticLayout instances if data will absent. In more serious situation you can create more abstract caching mechanism and create global instance in Application inheritor. But I decided, that for our example it’s too much.

You can see here commit with adding caching mechanism. Now let’s run application and see profiling results:

Profiling results for GameItemView with cache mechanism

As alternative to caching we can formulate StaticLayout synchronously with getting GameList content. For example, we have a query ‘GET …/games’ that return json with game list. When we parse this json we can formulate StaticLayouts for each item. But there could be some issues: memory and code flexibility, because game data is model part and StaticLayout is a view part that can change, if we, for example, will resize list.

Final improvements

According to final screenshot we possibility to make finishing touches. Let’s move TextUtils.ellipsize method to StaticLayout initialization in cache. And also we will use approach of same loading image placeholder. So there is no need to create new ColorDrawable every time setGame is invoked. Here you can find commit with final preparations. And let’s see final profiling results:

From 4 to 1.8 ms. Now what?

Quite reasonable question and remark. As I said this is small optimization that is relatively expensive in terms of saving 2 milliseconds. But still there are benefits of using custom view and particularly StaticLayout. For example, time of view creation is reduced, which means more responsiveness of application. Compare to screenshots (with xml layout and custom view):

Profiling results for inflating layout
Profiling results for creating custom view

Also, the time difference is more significant when we use, for example, list with long-text-items. Compare two screenshots for lists with big game descriptions:

Profiling results for binding layout
Profiling results for setGame method with cached StaticLayout

Afterwords

There are different ways to optimize working with views in Android. In particularly, for RecyclerView there are tons of optimizations. And one of them is using custom views for items. I agree it can be expensive work (to write custom view instead of simple and straightforward xml layout), but there are situation when it’s necessarily (for example, in messengers). And if you see a need of custom view for RecyclerView item, then consider to use StaticLayout for text. It will be faster than TextView and you will able to cache it, which can bring us to very fast scrolling.

--

--

Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways