Building for Android TV — Episode 2

Let’s go custom.

This second article will show a few key aspects that you need to consider if you want to build a custom UI for your TV application. While I won’t go through all the needed code (which is a considerable quantity), I will focus on the most important parts. After reading this, you should be able to circumvent the limitations posed by the standard components offered by the Leanback library.

The biggest limitation

In the previous episode, I anticipated that BrowseFragment is extremely useful but comes with a big limitation: you cannot map more than one row to a category. Let’s see this image again:

What happens here is that if you move from Category Zero to Category One on the left, you will also change the row on the right. There are many situations where this limitation is just too big to be ignored. For instance, you may have a category that has sub-categories; or you may want to display a custom fragment just for one category (e.g., a tutorial page, or a login page). And, apparently, I was not alone in banging my head on the problem: see this and this.

Let’s see the technicalities around this problem. When you use the BrowseFragment class, you need to provide an ArrayObjectAdapter that contains a list of HeaderItems and a collection of Rows. Since BrowseFragment expects only classes that inherit from Row, if you try to supply a Fragment you will incur in a ClassCastException.

For this reason, it would be great if we could achieve these two requirements simultaneously:

  1. Allow BrowseFragment to display custom Fragments
  2. Reuse pre-built components as much as possible

To understand if this is possible, we need to figure out what’s happening under the hood.

BrowseFragment = HeadersFragment + RowsFragment

The documentation states that a BrowseFragment is “composed of a RowsFragment and a HeadersFragment”; unfortunately, this is the only piece of useful information we get from the docs. Luckily, the Leanback support library comes with sources, so it’s quite easy to dig into the code and see the actual structure of BrowseFragment. In order to maximize the reuse of pre-existing components, the idea I had can be summed up as:

  1. Create a custom layout that contains a HeadersFragment and a RowsFragment
  2. Map every element of HeadersFragment to a RowsFragment
  3. Load the appropriate RowsFragment via the FragmentManager
  4. Handle focus over elements properly

Strangely enough, these points are ordered by difficulty. Let’s get through them one at a time.

Custom layout

This is the starting point. You just need to create a FrameLayout that has two types of children: a HeadersFragment and a RowsFragment. We will need to extend those classes, so let’s call them CustomHeadersFragment and CustomRowsFragment. The XML of this layout can be found here. You may have noticed that the CustomRowsFragment element has a 300dp left margin: this is necessary, since the FrameLayout will overlap its two children.

Now that we have our custom layout, let’s load our custom fragments in it. Once the activity_custom.xml layout is loaded in TVDemoActivity, create an instance of CustomHeadersFragment and a CustomRowsFragment, then load them on their respective containers. Check the new TVDemoActivity code and you will immediately see what I’m talking about.

Phase 1 is completed. Good job! If you run the application, you will see a familiar layout, staring right back at you. Now we need to populate that layout with some elements.

Custom mapping

For the sake of simplicity, we are going to populate our custom fragments with the same movies cards that Google provides in its original demo. To distinguish one fragment from the other, we’re going to assign a different background color to each one of them. All hail creativity ☺

First of all, we need the list of CustomRowsFragment that we want to display: these are created in TVDemoActivity.

To populate a CustomRowsFragment, we just need to create an ArrayObjectAdapter and fill it with as many rows as we like. In this case, the movies Google kindly provides us. This code is pretty straightforward: the loadRows() method does all the work, plus the setCustomFragmentColor() method adds a way of differentiating one custom fragment from the other.

Now the tricky part: we need to populate the CustomHeadersFragment by mapping each “header element” with a CustomRowsFragment. In addition, every time an header element is selected, we need to load the corresponding fragment via the FragmentManager. First things first, we create the header adapter with the elements: this is done in the setHeaderAdapter() method. It basically creates an HeaderItem and ties it, together with its corresponding CustomRowsFragment, to a ListRow element; this HeaderItem is then inserted into the main ArrayObjectAdapter.

We now need to switch fragment every time a new category is selected. To do so, we can set a selection listener via the.. [drum rolls] setOnItemSelectedListener() method! Given the category, we retrieve the associated fragment and we just load it using the FragmentManager. These two lines of code are all it takes.

And now, the icing on the cake. You have probably wondered how to customize the color of the HeadersFragment. I haven’t thoroughly looked for the proper way of doing so, hence I came up with some good Reflection over the (private) setBackgroundColor() method. Reflection is so fine!

Custom focus handling & UI adjustment

This part is probably the hardest one, and it deserves an article on its own. I will write about it on the next part of this series. Sorry chaps.

Conclusions

You should now be able to replicate the behavior of a BrowseFragment, with the exception that loading any type of Fragment is now possible. There are a few quirks that need to be taken care of: our CustomHeadersFragment does not close/open, the content is not padded correctly, the search orb is missing, .. But now the basic structure is there. And that’s a good thing.

This is what the custom BrowseFragment should look like.

The next episode deals with what’s missing, so be sure to check it out!

Code

All the code for this second article is available on GitHub here: Episode 2.