Distraction-Free Reading for your Android app — Part I

One app that I’ve had on every phone I’ve owned since 2013 is Pocket. If you haven’t heard of it, it lets you save any articles you come across online and read them later in the app, only with all distracting elements and visual noise (like popups, navigation and ads) removed, and with nicely-formatted text.

On top of that, all non-essential in-app UI elements, such as the status and navigation bars, fade away as you start reading. It’s basically the closest you’ll get to a paper book on your phone in terms of a focused reading experience:

Pocket’s Auto-Fullscreen mode in action

Recently I’ve starting working on an app for StarCraft II strategies. The app is relatively text-heavy and seemed to be a good match for this kind of feature, so I thought I’d have a go at building it.

Although definitely doable, getting everything to work seamlessly involved a decent amount of reading (and re-reading) of different blog posts and documentation, and a fair bit of trial-and-error. It took a while until certain parts “clicked” — particularly how android:fitsSystemWindows works.

Outline

This guide aims to make all of that more accessible, and hopefully save you time and frustration if you’re looking to build something similar. It’ll assume no more than a mid-beginner level of experience with Android development.

We’ll build a Pocket-like distraction-free reading experience with an iterative approach, starting with a basic activity layout, and adding distraction-free features, one at a time.

Part I (this article) will set up an app with some basic distraction-free features; if you’re an experienced developer with an existing app you might want to jump straight to Part II where we look into fitsSystemWindows to implement the more advanced features.

If you get stuck, there’s a link to the full source code for this part at the end of the article.

Onward…

What we’re going to build

  • A basic article view with a Toolbar on top, a translucent system navigation bar and a Floating Action Button (FAB) to make things a bit more interesting.
  • When the user scrolls down the article, the Toolbar, system bars and FAB (“chrome” collectively) will animate away all at once.
  • Scrolling up will animate the chrome back in.

Additional constraints to further reduce distractability:

  1. The text content should not jump or snap in any way when the chrome animates in or out. Chrome should fade away seamlessly, without disrupting the user.
  2. It should never be necessary for the user to trigger auto-hiding of chrome in order to read all parts of the article. In other words, it should still be possible to read the entire article even if chrome happened to get stuck in a permanently-visible state (this is actually related to the previous constraint — more on that later).

Getting Started

Let’s dive in. Open up Android Studio and create a new project with a blank activity.

Theme

By default your app’s theme will inherit from Theme.AppCompat.Light.DarkActionBar which will automatically add an Action bar/Toolbar to your activity windows. We’re going to want to add the Toolbar to our Activity layout manually to precisely control its placement, so this isn’t what we want. Change your AppTheme to inherit from AppCompat’s NoActionBar theme instead:

app/src/main/res/values/styles.xml

Build Dependencies

Since we want a FAB we’ll need to add a dependency on Google’s Design Support Library. Add this to the dependencies section of your app module’s build script:

app/build.gradle:

compile 'com.android.support:design:25.3.1'

(Where 25.3.1 is the same version as the other support libraries in your build script — this might differ in your generated project)

Strings

We’ll need some dummy article content for our reader activity, and enough of it to extend outside of the screen to make the view scrollable.

Grab some Lorem ipsum text and paste it into your strings file as a new string resource:

app/src/main/res/strings.xml:

<string name="lorem">Lorem ipsum dolor sit amet ... </string>

(This is also a good time to change your app_name string to something else if you feel so inclined).

Initial Activity Layout

Open up activity_main.xml and replace whatever’s in there with this:

app/src/main/res/layout/activity_main.xml

A quick refresher on FrameLayout

So what’s going on here is we have two full-size (i.e. width and height set to match_parent) view groups stacked on top of each other inside a root FrameLayout. The first view group is for the article text (the NestedScrollView) and the second is for the UI chrome (the inner FrameLayout).

Due to their order in the root FrameLayout, the UI chrome layer will always be drawn over the text layer. One way to remember how FrameLayout works is to imagine it painting Views in the order they’re listed— the last-mentioned View will always be painted last, over top of everything else.

Now you might be thinking this setup would result in the TextView being obscured by the Toolbar, violating constraint #2 from earlier. You would be right. To avoid that, the TextView’s top margin has been set to the default actionbar height, ensuring it will appear just below the bottom of the Toolbar when the activity first opens.

Initial Activity Code

There isn’t much code to write at the point, all we need to do is ensure some text is displayed in the Toolbar.

Open up MainActivity.java and replace the default code with this:

app/src/main/java/<com.yourpackagehere>/MainActivity.java

This will cause the Toolbar’s title to be whatever you chose for the app_name string.

If you go ahead and Run (Ctrl+R on Mac) the app now you should see something like this:

Our inappropriately-titled first version

This isn’t exactly the embodiment of a distraction-free reading experience yet. Let’s start fixing that.

Removing Distractions, One Step at a Time

Disappearing FAB

It turns out hiding the FAB when the user scrolls is straightforward enough, so let’s start there. Jump back into editing MainActivity in Android Studio and add a new field for the FAB:

private FloatingActionButton actionButton;

The FAB has an id assigned in the activity’s layout XML, we can use that to find and save a reference to it in onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
actionButton = (FloatingActionButton) findViewById(R.id.fab);
setupToolbar();
}

We can use this reference to hide and show the FAB when needed in code. The next step is to actually do that at the correct times, namely, when the NestedScrollView is scrolled up or down.

Listening out for Scroll direction changes

NestedScrollView can have an OnScrollChangeListener set on it via its aptly-named setOnScrollChangeListener() method. This listener gives us all of the info we need (and then some!) to know whether to hide or show the FAB via its single callback method:

void onScrollChange (NestedScrollView v, 
int scrollX,
int scrollY,
int oldScrollX,
int oldScrollY)

We happen to know that horizontal scrolling isn’t possible in our layout, so we can ignore scrollX and oldScrollX.

scrollY and oldScrollY contain the absolute scroll positions for the view. What we’re interested in is not so much absolute values, but when the scroll direction changes. I.e. when the user begins scrolling down initially, and when they go from scrolling down (possibly intermittently) to scrolling up.

In other words, we’re interested in when scrollY - oldScrollY (let’s call this yDelta) goes from zero or negative, to something positive, and vice versa.

This implies storing an oldYDelta somewhere, and then comparing a newly-calculated yDelta against it each time there’s a scroll event. We could store this state in a field in the activity, but activities have a way of accumulating more and more variables and code until they become a confusing mess.

This is a good opportunity to create a small, reusable class. In Android Studio, create a new abstract class called OnScrollDirectionChangedListener:

app/src/main/java/<com.yourpackagehere>/OnScrollDirectionChangedListener.java

This class encapsulates all of the logic and state needed to calculate scroll direction changes and surfaces it through a higher-level, easier-to-use interface — its onStartScrollingDown() and onStartScrollingUp() callback methods.

Let’s make use of this in MainActivity and see how it looks. Jump back into MainActivity and add a new method called setupScrollView():

private void setupScrollView() {
NestedScrollView scrollView =
(NestedScrollView) findViewById(R.id.nested_scrollview);

scrollView.setOnScrollChangeListener(
new OnScrollDirectionChangedListener() {
@Override
public void onStartScrollingDown() {
actionButton.hide();
}

@Override
public void onStartScrollingUp() {
actionButton.show();
}
});
}

Add a call to it in onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
actionButton = (FloatingActionButton) findViewById(R.id.fab);
setupToolbar();
setupScrollView();
}

That’s all for the FAB. If you run the app (Ctrl+R) now you should find that the FAB shrinks away to nothing when you scroll down, and zooms back into existence when you scroll up. FloatingActionButton is unusual in that it comes with these animations out of the box — a nice touch by the library developers.

Disappearing Toolbar

Since we have a scroll listener set up, it’s not too much extra work to get the Toolbar to appear and re-appear as well.

Now we could just set the Toolbar’s visibility to GONE on scroll down, and back to VISIBLE on scroll up, but having visuals instantly pop in and out isn’t a great user experience. Also, it would violate that part in constraint #1 about “chrome fading away seamlessly”.

No, we can do better. Let’s actually animate it.

The Toolbar class doesn’t come with built-in animations like FloatingActionButton, so we’ll add our own.

We will create two new animation XML files in app/src/main/res/anim (create the directory first if it doesn’t exist) — one for animating the Toolbar out of view, and one for bringing it back in.

Now jump back to MainActivity and create two new methods for hiding and showing the Toolbar using these animations:

app/src/main/java/<com.yourpackagehere>/MainActivity.java

Note that we have to change the Toolbar’s visibility at the end and beginning of these animations via AnimationListeners. If we don’t do this, they will immediately return to their pre-animation states when the animations end. Remember: just because an animation causes a View to fade out, doesn’t mean the View will stay faded-out :)

Add calls to these new methods from the same scroll direction callbacks we used earlier for the FAB:

app/src/main/java/<com.yourpackagehere>/MainActivity.java

Let’s see how it looks now:

Progress! We’re halfway to building a respectable Pocket clone (well… kindof).

All that’s left are the system bars — the status bar on top and navigation bar on the bottom.

We’ll tackle that in Part II by learning about fitsSystemWindows, window flags, decor views and other dark corners of the Android SDK.

You can find the source code for Part 1 on github.

Hope this helped, stayed tuned for Part II in this series!

Further Discussion

Q: Why not use CoordinatorLayout + AppBarLayout from the design support library to hide the toolbar on scrolls?

A: This is a valid question — the design support library provides an alternative way to do everything we’ve seen in the sample until now with a combination of CoordinatorLayout, AppBarLayout, NestedScrollView and custom Behaviors. It’s good to use existing solutions where possible to avoid reinventing the wheel, and in fact this is where I started. Ultimately however, I moved away from using CoordinatorLayout for a number of reasons.

CoordinatorLayout + AppBarLayout gives you a lot of power and options for animating the Toolbar in response to scroll events. This includes having it scroll out of view in direct relation to scrolls on the text, rather than hiding with a fixed-duration animation. This is a nice effect, and if you want this it does probably make sense to use the library.

However, you’ll notice that Pocket opts for a fixed-duration Toolbar animation. I think this makes sense given the goals for their app: if you want to hide all non-essential UI elements in an non-distracting way, it makes sense to have them all hide together at the same speed. In addition, for reading apps like this it makes sense to hide chrome as soon as possible once the user has indicated their interest in the text via scrolling, rather than letting it linger.

As we’ve seen, setting up a scroll listener to run a fixed-duration animation on the Toolbar yourself is straightforward and requires very little code. It’s easy to see what’s going on, even to developers new to the platform.

I don’t believe the same can be said about the CoordinatorLayout Behavior approach. If you’re anything like me and other Android developers I’ve worked with, you’ll find yourself referring back to documentation to understand the effect of the different app bar layout behaviors each time you encounter them. The names aren’t self-documenting.

If and when you need to implement more complex animations in your layout in response to scroll events, the extra power offered by the library might be exactly what you need. For simple cases like this however, the trade-off in complexity and readability probably isn’t worth it.

Remember the KISS and YAGNI principles.

Q: What about just using a custom CoordinatorLayout Behavior to hide the FAB?

Yes that can be done — the recommended approach was to use CoordinatorLayout as your root layout, create a new ScrollAwareFABBehavior class that extends FloatingActionButton.Behavior, setting it as the FAB’s layout_behavior in XML, and this will work fine.

That is, it did work fine until v25.1.0 of the Design Support Library. This version introduced a breaking change whereby Views whose visibility is GONE would no longer received nested scroll events. This is true for FABs after hide() is called, meaning your FAB would hide once and never reappear.

The recommended solution/workaround was to create a new Behavior for the scrolling view itself to iterate through all of the CoordinatorLayout’s child Views, checking if they’re FABs via runtime type-checking, and then animating them if they are.

Even ignoring potential performance issues of this approach, I found this to be a needlessly complex and opaque solution when contrasted with simply calling hide() or show() on the FAB in a scroll listener.