I recently started working on a Bible app. As you can imagine, there is a lot of text, and I quickly ran into a problem where my app was dropping frames while scrolling through that text.

The UI consisted of a RecyclerView where each row was a custom TextView.

Since the client wanted a continuous scroll experience and all 31,142 verses of the Bible from Genesis 1:1 to Revelation 22:21 is a sizable dataset, I didn't want to keep it all in memory. When a row is bound in the RecyclerView.Adapter<> there is an asynchronous call to get the text for that row, and when onCompleted() is called on the rx.Observer I called setText() on a TextView.

Very simple, really.

1. Bind to a row
public class BibleChapterAdapter extends RecyclerView.Adapter<BibleChapterViewHolder> {

    private FormattedChapterRetiever formattedChapterRetiever;

    ...

    public void onBindViewHolder(BibleChapterViewHolder holder, int position) {

        if (holder.getSubscription() != null && !holder.getSubscription().isUnsubscribed()) {
            holder.getSubscription().unsubscribe();
        }

        final BibleReference location = bookPicker.getChapters().get(position);

        holder.setSubscription(

                formattedChapterRetiever.getFormattedChapterTextObservable(location)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(holder)

        );

    }

    ...

}
2. Retreive the text
public class FormattedChapterRetiever {

    ...

    public Observable<FormattedChapterText> getFormattedChapterTextObservable(final BibleReference location) {

        return Observable.create(new Observable.OnSubscribe<FormattedChapterText>() {
            @Override
            public void call(Subscriber<? super FormattedChapterText> subscriber) {
                try {
                    FormattedChapterText formattedChapterText = formattedChapterTextCache.get(location.toString());

                    if (formattedChapterText == null) {
                        ChapterContainer container = bookParser.getChapter(location);
                        formattedChapterText = chapterFormatter.getFormattedChapter(container);

                        formattedChapterTextCache.put(location.toString(), formattedChapterText);
                    }

                    subscriber.onNext(formattedChapterText);
                    subscriber.onCompleted();
                } catch (Exception e) {
                    subscriber.onError(e);
                }

            }
        });
    }

    ...

}
3. Call setText() on the TextView
public class BibleChapterViewHolder extends RecyclerView.ViewHolder implements Observer<FormattedChapterText> {

    private FormattedChapterText chapterText;

    //extends TextView
    @Bind(R.id.lblBibleChapter) BibleChapterView bibleChapterTextView;  

    ...

    @Override
    public void onCompleted() {
        bibleChapterTextView.setText(chapterText.getSpannedText(), TextView.BufferType.SPANNABLE);

        ...
    }

    ...
}

So simple that I was a little shocked that my scroll performance was so bad it made the app nearly unusable on some devices (Nexus 7 2013). How could this be? My layouts were simple, nothing crazy.

I turned to Profile GPU Rendering to get some answers. Here is what I saw:

Hm. All blue, and we know that blue:

...represents the time used to create and update the View's display lists. If this part of the bar is tall, there may be a lot of custom view drawing, or a lot of work in onDraw methods.

Ok, well, even though the ViewGroup wasn't that complex, the TextView was a bit complicated. It used a custom typeface of Harriet that has a lot of embellishments, and there are many spans for headings, footnotes, images, etc. I couldn't make the TextView less complex, so I needed to figure something else out.

StaticLayout

I then came across StaticLayout. StaticLayout is a Layout for text that will not be edited after it is laid out. This is actually what TextView is using to layout text, it's just using it inefficiently. I also learned that in addition to the cost of drawing text, measuring text is very expensive.

If you've been developing for Android for any length of time you'll know that you cannot access View's unless you're on the main thread/UI thread. Thus rendering the text to the TextView in a background thread was out of the question. With StaticLayout though, I could keep the cost of measuring and laying out text in a background thread, passing the StaticLayout to the main thread when it was ready to be drawn on the screen. Could this solve my blue line problem?

Pretty much, maybe needs a little more work, but worlds better. It feels very smooth now.

The code

I had originally changed my TextView to an ImageView and was just rendering the StaticLayout to a Bitmap and calling ImageView.setImageBitmap(). This worked, but caused me to create a lot of different sized bitmaps, which isn't great for avoiding garbage collection while scrolling. I then went the Instagram route and just extended View and overrode onDraw, drawing my StaticLayout to the View's Canvas.

public class BibleChapterView extends View {

    private StaticLayout staticLayout;

    ...

    public void setStaticLayout(StaticLayout layout) {

        this.staticLayout = layout;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();

        if (staticLayout != null) {
            staticLayout.draw(canvas);
        }

        canvas.restore();
    }

    ...
}

This does have the unfortunate side effect of me having to set my StaticLayout to null when the BibleChapterView recycles, so it doesn't draw the wrong text for the row. Hopefully I find a way to make this better.

The rest of the code basically works the same way with slight modifications to use StaticLayout instead of TextView

1. Bind to a row, setting the StaticLayout to null
public class BibleChapterAdapter extends RecyclerView.Adapter<BibleChapterViewHolder> {

    private FormattedChapterRetiever formattedChapterRetiever;

    ...

    @Override
    public void onBindViewHolder(BibleChapterViewHolder holder, int position) {

        if (holder.getSubscription() != null && !holder.getSubscription().isUnsubscribed()) {
            holder.getSubscription().unsubscribe();
        }

        final BibleReference location = chapters.get(position);

        holder.bibleChapterTextView.setStaticLayout(null);
        holder.setSubscription(

                formattedChapterRetiever.getFormattedChapterTextObservable(location)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(holder)

        );

    }

    ...

}
2. Retreive the text, but return a StaticLayout instead of just the SpannedText
public class FormattedChapterRetiever {

    ...

    public Observable<StaticLayout> getFormattedChapterTextObservable(final BibleReference location) {

        return Observable.create(new Observable.OnSubscribe<StaticLayout>() {
            @Override
            public void call(Subscriber<? super StaticLayout> subscriber) {
                try {
                    FormattedChapterText formattedChapterText = formattedChapterTextCache.get(location.toString());

                    if (formattedChapterText == null) {
                        ChapterContainer container = bookParser.getChapter(location);
                        formattedChapterText = chapterFormatter.getFormattedChapter(container);

                        formattedChapterTextCache.put(location.toString(), formattedChapterText);
                    }

                    TextPaint textPaint = new TextPaint(getBibleTextColor());
                    textPaint.setTypeface(getBibleTextFont());
                    textPaint.setAntiAlias(true);

                    StaticLayout staticLayout = new StaticLayout(formattedChapterText.getSpannedText(), 
                                                            textPaint, 
                                                            screenWidth, 
                                                            Layout.Alignment.ALIGN_NORMAL, 
                                                            spacingMultiplier, 
                                                            spacingAdd, 
                                                            false);

                    subscriber.onNext(staticLayout);
                    subscriber.onCompleted();
                } catch (Exception e) {
                    subscriber.onError(e);
                }

            }
        });

    }
}
3. Set the StaticLayout to the ChapterBibleView so it will be drawn
public class BibleChapterViewHolder extends RecyclerView.ViewHolder implements Observer<FormattedChapterText> {

    private FormattedChapterText chapterText;

    @Bind(R.id.lblBibleChapter) BibleChapterView bibleChapterTextView;  

    ...

    @Override
    public void onCompleted() {
        bibleChapterTextView.setStaticLayout(renderedChapterLayout);

        ViewGroup.LayoutParams params = bibleChapterTextView.getLayoutParams();
        params.height = renderedChapterLayout.getHeight();

        ...
    }

    ...
}

This approach worked great for me. Mostly because it took some of the expensive parts of rendering text off the main thread, so that I could avoid dropping frames.

If you have any suggestions on how I might improve my code, please share.

Thanks for reading!